/*
 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
 *           (C) 1999 Antti Koivisto (koivisto@kde.org)
 * Copyright (C) 2004, 2005, 2006, 2007, 2009, 2010 Apple Inc. All rights
 * reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "third_party/blink/renderer/core/loader/image_loader.h"

#include <memory>
#include <utility>

#include "services/network/public/mojom/web_client_hints_types.mojom-blink.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/platform/web_url_request.h"
#include "third_party/blink/renderer/bindings/core/v8/script_controller.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/css/css_primitive_value.h"
#include "third_party/blink/renderer/core/css/css_property_name.h"
#include "third_party/blink/renderer/core/css/css_property_value_set.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/increment_load_event_delay_count.h"
#include "third_party/blink/renderer/core/execution_context/agent.h"
#include "third_party/blink/renderer/core/frame/deprecation/deprecation.h"
#include "third_party/blink/renderer/core/frame/frame_owner.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/html/cross_origin_attribute.h"
#include "third_party/blink/renderer/core/html/html_embed_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_object_element.h"
#include "third_party/blink/renderer/core/html/html_picture_element.h"
#include "third_party/blink/renderer/core/html/loading_attribute.h"
#include "third_party/blink/renderer/core/html/media/html_video_element.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_image.h"
#include "third_party/blink/renderer/core/layout/layout_video.h"
#include "third_party/blink/renderer/core/layout/svg/layout_svg_image.h"
#include "third_party/blink/renderer/core/loader/fetch_priority_attribute.h"
#include "third_party/blink/renderer/core/loader/lazy_image_helper.h"
#include "third_party/blink/renderer/core/probe/async_task_context.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/core/svg/graphics/svg_image.h"
#include "third_party/blink/renderer/core/svg/graphics/svg_image_for_container.h"
#include "third_party/blink/renderer/core/timing/soft_navigation_heuristics.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/v8_per_isolate_data.h"
#include "third_party/blink/renderer/platform/graphics/bitmap_image.h"
#include "third_party/blink/renderer/platform/heap/cross_thread_handle.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_parameters.h"
#include "third_party/blink/renderer/platform/loader/fetch/memory_cache.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_loading_log.h"
#include "third_party/blink/renderer/platform/scheduler/public/event_loop.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/weborigin/security_policy.h"

namespace blink {

namespace {

// This implements the HTML Standard's list of available images tuple-matching
// logic [1]. In our implementation, it is only used to determine whether or not
// we should skip queueing the microtask that continues the rest of the image
// loading algorithm. But the actual decision to reuse the image is determined
// by ResourceFetcher, and is much stricter.
// [1]:
// https://html.spec.whatwg.org/multipage/images.html#updating-the-image-data:list-of-available-images
bool CanReuseFromListOfAvailableImages(
    const Resource* resource,
    CrossOriginAttributeValue cross_origin_attribute,
    const SecurityOrigin* origin) {
  const ResourceRequestHead& request = resource->GetResourceRequest();
  bool is_same_origin = request.RequestorOrigin()->IsSameOriginWith(origin);
  if (cross_origin_attribute != kCrossOriginAttributeNotSet && !is_same_origin)
    return false;

  if (request.GetCredentialsMode() ==
          network::mojom::CredentialsMode::kSameOrigin &&
      cross_origin_attribute != kCrossOriginAttributeAnonymous) {
    return false;
  }

  return true;
}

}  // namespace

class ImageLoader::Task {
 public:
  Task(ImageLoader* loader, UpdateFromElementBehavior update_behavior)
      : loader_(loader), update_behavior_(update_behavior) {
    ExecutionContext* context = loader_->GetElement()->GetExecutionContext();
    async_task_context_.Schedule(context, "Image",
                                 probe::AsyncTaskContext::StackOptions::kScan);
    world_ = context->GetCurrentWorld();
  }

  void Run() {
    if (!loader_)
      return;
    ExecutionContext* context = loader_->GetElement()->GetExecutionContext();
    probe::AsyncTask async_task(context, &async_task_context_);
    loader_->DoUpdateFromElement(world_.Get(), update_behavior_);
  }

  void ClearLoader() {
    loader_ = nullptr;
    world_ = nullptr;
  }

  base::WeakPtr<Task> GetWeakPtr() { return weak_factory_.GetWeakPtr(); }

 private:
  WeakPersistent<ImageLoader> loader_;
  UpdateFromElementBehavior update_behavior_;
  Persistent<const DOMWrapperWorld> world_;

  probe::AsyncTaskContext async_task_context_;
  base::WeakPtrFactory<Task> weak_factory_{this};
};

ImageLoader::ImageLoader(Element* element)
    : element_(element),
      image_complete_(true),
      suppress_error_events_(false),
      lazy_image_load_state_(LazyImageLoadState::kNone) {
  RESOURCE_LOADING_DVLOG(1) << "new ImageLoader " << this;
}

ImageLoader::~ImageLoader() = default;

void ImageLoader::Dispose() {
  RESOURCE_LOADING_DVLOG(1)
      << "~ImageLoader " << this
      << "; has pending load event=" << pending_load_event_.IsActive()
      << ", has pending error event=" << pending_error_event_.IsActive();

  if (image_content_) {
    delay_until_image_notify_finished_ = nullptr;
  }
}

static bool ImageTypeNeedsDecode(const Image& image) {
  // SVG images are context sensitive, and decoding them without the proper
  // context will just end up wasting memory (and CPU).
  // TODO(vmpstr): Generalize this to be all non-lazy decoded images.
  if (IsA<SVGImage>(image))
    return false;
  return true;
}

void ImageLoader::DispatchDecodeRequestsIfComplete() {
  // If the current image isn't complete, then we can't dispatch any decodes.
  // This function will be called again when the current image completes.
  if (!image_complete_)
    return;

  bool is_active = GetElement()->GetDocument().IsActive();
  // If any of the following conditions hold, we either have an inactive
  // document or a broken/non-existent image. In those cases, we reject any
  // pending decodes.
  if (!is_active || !GetContent() || GetContent()->ErrorOccurred()) {
    RejectPendingDecodes();
    return;
  }

  LocalFrame* frame = GetElement()->GetDocument().GetFrame();
  EraseIf(decode_requests_, ([&](const auto& request) {
            // If the image already in kDispatched state or still in
            // kPendingMicrotask
            // state, then we don't dispatch decodes for it. So, the only
            // case to handle is if we're in kPendingLoad state.
            if (request->state() != DecodeRequest::kPendingLoad) {
              return false;
            }
            Image* image = GetContent()->GetImage();
            if (!ImageTypeNeedsDecode(*image)) {
              // If the image is of a type that doesn't need decode,
              // resolve the promise.
              request->Resolve();
              return true;
            }
            cc::DrawImage draw_image(
                image->PaintImageForCurrentFrame(),
                /*use_dark_mode=*/false,
                SkIRect::MakeWH(image->width(), image->height()),
                cc::PaintFlags::FilterQuality::kNone, SkM44());
            // ImageLoader should be kept alive when decode is still
            // pending. JS may invoke 'decode' without capturing the Image
            // object. If GC kicks in, ImageLoader will be destroyed,
            // leading to unresolved/unrejected Promise.
            frame->GetChromeClient().RequestDecode(
                frame, draw_image,
                BindOnce(&ImageLoader::DecodeRequestFinished,
                         MakeUnwrappingCrossThreadHandle(this),
                         request->request_id()),
                /*speculative*/ false);
            request->NotifyDecodeDispatched();
            return false;
          }));
}

void ImageLoader::DecodeRequestFinished(uint64_t request_id, bool success) {
  // First we find the corresponding request id, then we either resolve or
  // reject it and remove it from the list.
  auto it = std::find_if(decode_requests_.begin(), decode_requests_.end(),
                         [request_id](const auto& request) {
                           return request->request_id() == request_id;
                         });

  if (it != decode_requests_.end()) {
    auto& request = *it;
    if (success) {
      request->Resolve();
    } else {
      request->Reject();
    }
    decode_requests_.erase(it);
  }
}

void ImageLoader::RejectPendingDecodes(UpdateType update_type) {
  // Normally, we only reject pending decodes that have passed the
  // kPendingMicrotask state, since pending mutation requests still have an
  // outstanding microtask that will run and might act on a different image than
  // the current one. However, as an optimization, there are cases where we
  // synchronously update the image (see UpdateFromElement). In those cases, we
  // have to reject even the pending mutation requests because conceptually they
  // would have been scheduled before the synchronous update ran, so they
  // referred to the old image.
  EraseIf(decode_requests_, ([&](const auto& request) {
            if (update_type == UpdateType::kAsync &&
                request->state() == DecodeRequest::kPendingMicrotask) {
              return false;
            }
            request->Reject();
            return true;
          }));
}

void ImageLoader::Trace(Visitor* visitor) const {
  visitor->Trace(image_content_);
  visitor->Trace(image_content_for_image_document_);
  visitor->Trace(element_);
  visitor->Trace(decode_requests_);
  ImageResourceObserver::Trace(visitor);
}

void ImageLoader::SetImageForTest(ImageResourceContent* new_image) {
  DCHECK(new_image);
  SetImageWithoutConsideringPendingLoadEvent(new_image);
  // This is needed if the `new_image` is pending, since setting the image this
  // way will only update the layout object if the image is fully loaded.
  UpdateLayoutObject();
}

bool ImageLoader::ImageIsPotentiallyAvailable() const {
  bool is_lazyload = lazy_image_load_state_ == LazyImageLoadState::kDeferred;

  bool image_has_loaded = image_content_ && !image_content_->IsLoading() &&
                          !image_content_->ErrorOccurred();
  bool image_still_loading = !image_has_loaded && HasPendingActivity() &&
                             !HasPendingError() &&
                             !element_->ImageSourceURL().empty();
  bool image_has_image = image_content_ && image_content_->HasImage();
  bool image_is_document = element_->GetDocument().IsImageDocument() &&
                           image_content_ && !image_content_->ErrorOccurred();

  // Icky special case for deferred images:
  // A deferred image is not loading, does have pending activity, does not
  // have an error, but it does have an ImageResourceContent associated
  // with it, so |image_has_loaded| will be true even though the image hasn't
  // actually loaded. Fixing the definition of |image_has_loaded| isn't
  // sufficient, because a deferred image does have pending activity, does not
  // have a pending error, and does have a source URL, so if |image_has_loaded|
  // was correct, |image_still_loading| would become wrong.
  //
  // Instead of dealing with that, there's a separate check that the
  // ImageResourceContent has non-null image data associated with it, which
  // isn't folded into |image_has_loaded| above.
  return (image_has_loaded && image_has_image) || image_still_loading ||
         image_is_document || is_lazyload;
}

void ImageLoader::ClearImage() {
  SetImageWithoutConsideringPendingLoadEvent(nullptr);
}

void ImageLoader::SetImageWithoutConsideringPendingLoadEvent(
    ImageResourceContent* new_image_content) {
  DCHECK(failed_load_url_.empty());
  ImageResourceContent* old_image_content = image_content_.Get();
  if (new_image_content != old_image_content) {
    if (pending_load_event_.IsActive())
      pending_load_event_.Cancel();
    if (pending_error_event_.IsActive())
      pending_error_event_.Cancel();
    UpdateImageState(new_image_content);
    if (new_image_content) {
      new_image_content->AddObserver(this);
    }
    if (old_image_content) {
      old_image_content->RemoveObserver(this);
    }
  }

  ResetAnimation();
}

static void ConfigureRequest(
    FetchParameters& params,
    Element& element,
    const ClientHintsPreferences& client_hints_preferences) {
  CrossOriginAttributeValue cross_origin = GetCrossOriginAttributeValue(
      element.FastGetAttribute(html_names::kCrossoriginAttr));
  if (cross_origin != kCrossOriginAttributeNotSet) {
    params.SetCrossOriginAccessControl(
        element.GetExecutionContext()->GetSecurityOrigin(), cross_origin);
  }

  mojom::blink::FetchPriorityHint fetch_priority_hint =
      GetFetchPriorityAttributeValue(
          element.FastGetAttribute(html_names::kFetchpriorityAttr));
  params.SetFetchPriorityHint(fetch_priority_hint);

  auto* html_image_element = DynamicTo<HTMLImageElement>(element);
  if ((client_hints_preferences.ShouldSend(
           network::mojom::WebClientHintsType::kResourceWidth_DEPRECATED) ||
       client_hints_preferences.ShouldSend(
           network::mojom::WebClientHintsType::kResourceWidth)) &&
      html_image_element) {
    params.SetResourceWidth(html_image_element->GetResourceWidth());
  }
}

inline void ImageLoader::QueuePendingErrorEvent() {
  // The error event should not fire if the image data update is a result of
  // environment change.
  // https://html.spec.whatwg.org/C/#the-img-element:the-img-element-55
  if (suppress_error_events_) {
    return;
  }
  // There can be cases where QueuePendingErrorEvent() is called when there
  // is already a scheduled error event for the previous load attempt.
  // In such cases we cancel the previous event (by overwriting
  // |pending_error_event_|) and then re-schedule a new error event here.
  // crbug.com/722500
  pending_error_event_ = PostCancellableTask(
      *GetElement()->GetDocument().GetTaskRunner(TaskType::kDOMManipulation),
      FROM_HERE,
      BindOnce(&ImageLoader::DispatchPendingErrorEvent, WrapPersistent(this),
               std::make_unique<IncrementLoadEventDelayCount>(
                   GetElement()->GetDocument())));
}

inline void ImageLoader::CrossSiteOrCSPViolationOccurred(
    AtomicString image_source_url) {
  failed_load_url_ = image_source_url;
}

inline void ImageLoader::ClearFailedLoadURL() {
  failed_load_url_ = AtomicString();
}

inline void ImageLoader::EnqueueImageLoadingMicroTask(
    UpdateFromElementBehavior update_behavior) {
  auto task = std::make_unique<Task>(this, update_behavior);
  pending_task_ = task->GetWeakPtr();
  element_->GetDocument().GetAgent().event_loop()->EnqueueMicrotask(
      BindOnce(&Task::Run, std::move(task)));
  delay_until_do_update_from_element_ =
      std::make_unique<IncrementLoadEventDelayCount>(element_->GetDocument());
}

void ImageLoader::UpdateImageState(ImageResourceContent* new_image_content) {
  image_content_ = new_image_content;
  if (!new_image_content) {
    image_content_for_image_document_ = nullptr;
    image_complete_ = true;
    if (lazy_image_load_state_ == LazyImageLoadState::kDeferred) {
      LazyImageHelper::StopMonitoring(GetElement());
      lazy_image_load_state_ = LazyImageLoadState::kNone;
    }
  } else {
    image_complete_ = false;
    if (lazy_image_load_state_ == LazyImageLoadState::kDeferred) {
      if (new_image_content->IsLoaded() &&
          RuntimeEnabledFeatures::LazyImageConformantLoadEventTimingEnabled()) {
        LazyImageHelper::StopMonitoring(GetElement());
        lazy_image_load_state_ = LazyImageLoadState::kFullImage;
        EnqueueImageLoadingMicroTask(kUpdateNormal);
      } else {
        LazyImageHelper::StartMonitoring(GetElement());
      }
    }
  }
  delay_until_image_notify_finished_ = nullptr;
}

void ImageLoader::DoUpdateFromElement(const DOMWrapperWorld* world,
                                      UpdateFromElementBehavior update_behavior,
                                      const KURL* source_url,
                                      UpdateType update_type,
                                      bool force_blocking) {
  // FIXME: According to
  // http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#the-img-element:the-img-element-55
  // When "update image" is called due to environment changes and the load
  // fails, onerror should not be called. That is currently not the case.
  //
  // We don't need to call clearLoader here: Either we were called from the
  // task, or our caller updateFromElement cleared the task's loader (and set
  // pending_task_ to null).
  pending_task_.reset();
  // Make sure to only decrement the count when we exit this function
  std::unique_ptr<IncrementLoadEventDelayCount> load_delay_counter;
  load_delay_counter.swap(delay_until_do_update_from_element_);

  Document& document = element_->GetDocument();
  if (!document.IsActive()) {
    // Clear if the loader was moved into a not fully active document - or the
    // document was detached - after the microtask was queued. If moved into a
    // not fully active document, ElementDidMoveToNewDocument() will have
    // called ClearImage() already, but in the case of a detached document it
    // won't have.
    ClearImage();
    return;
  }

  KURL url;
  AtomicString image_source_url = element_->ImageSourceURL();
  if (base::FeatureList::IsEnabled(features::kOptimizeHTMLElementUrls) &&
      source_url) {
    url = *source_url;
  } else {
    url = ImageSourceToKURL(image_source_url);
  }
  ImageResourceContent* new_image_content = nullptr;
  if (!url.IsNull() && !url.IsEmpty()) {
    // Unlike raw <img>, we block mixed content inside of <picture> or
    // <img srcset>.
    ResourceLoaderOptions resource_loader_options(std::move(world));
    resource_loader_options.initiator_info.name = GetElement()->localName();
    ResourceRequest resource_request(url);
    if (update_behavior == kUpdateForcedReload) {
      resource_request.SetCacheMode(mojom::blink::FetchCacheMode::kBypassCache);
    }

    network::mojom::ReferrerPolicy referrer_policy =
        network::mojom::ReferrerPolicy::kDefault;
    AtomicString referrer_policy_attribute =
        element_->FastGetAttribute(html_names::kReferrerpolicyAttr);
    if (!referrer_policy_attribute.IsNull()) {
      SecurityPolicy::ReferrerPolicyFromString(
          referrer_policy_attribute, kSupportReferrerPolicyLegacyKeywords,
          &referrer_policy);
    }
    resource_request.SetReferrerPolicy(referrer_policy);

    // Correct the RequestContext if necessary.
    if (IsA<HTMLPictureElement>(GetElement()->parentNode()) ||
        GetElement()->FastHasAttribute(html_names::kSrcsetAttr)) {
      resource_request.SetRequestContext(
          mojom::blink::RequestContextType::IMAGE_SET);
      resource_request.SetRequestDestination(
          network::mojom::RequestDestination::kImage);
    } else if (IsA<HTMLObjectElement>(GetElement())) {
      resource_request.SetRequestContext(
          mojom::blink::RequestContextType::OBJECT);
      resource_request.SetRequestDestination(
          network::mojom::RequestDestination::kObject);
    } else if (IsA<HTMLEmbedElement>(GetElement())) {
      resource_request.SetRequestContext(
          mojom::blink::RequestContextType::EMBED);
      resource_request.SetRequestDestination(
          network::mojom::RequestDestination::kEmbed);
    }

    DCHECK(document.GetFrame());
    auto* frame = document.GetFrame();

    bool page_is_being_dismissed =
        document.PageDismissalEventBeingDispatched() != Document::kNoDismissal;
    if (page_is_being_dismissed) {
      resource_request.SetHttpHeaderField(http_names::kCacheControl,
                                          AtomicString("max-age=0"));
      resource_request.SetKeepalive(true);
      resource_request.SetRequestContext(
          mojom::blink::RequestContextType::PING);
      UseCounter::Count(document, WebFeature::kImageLoadAtDismissalEvent);
    }

    // Plug-ins should not load via service workers as plug-ins may have their
    // own origin checking logic that may get confused if service workers
    // respond with resources from another origin.
    // https://w3c.github.io/ServiceWorker/#implementer-concerns
    auto* html_element = DynamicTo<HTMLElement>(GetElement());
    if (html_element && html_element->IsPluginElement()) {
      resource_request.SetSkipServiceWorker(true);
    }

    FetchParameters params(std::move(resource_request),
                           resource_loader_options);

    ConfigureRequest(params, *element_, frame->GetClientHintsPreferences());

    if (update_behavior != kUpdateForcedReload &&
        lazy_image_load_state_ != LazyImageLoadState::kFullImage) {
      if (auto* html_image = DynamicTo<HTMLImageElement>(GetElement())) {
        if (LazyImageHelper::ShouldDeferImageLoad(*frame, html_image)) {
          lazy_image_load_state_ = LazyImageLoadState::kDeferred;
          params.SetLazyImageDeferred();
        }
      }
    }

    // If we're now loading in a once-deferred image, make sure it doesn't
    // block the load event.
    if (lazy_image_load_state_ == LazyImageLoadState::kFullImage &&
        !force_blocking) {
      params.SetLazyImageNonBlocking();
    }

    new_image_content = ImageResourceContent::Fetch(params, document.Fetcher());

    // If this load is starting while navigating away, treat it as an auditing
    // keepalive request, and don't report its results back to the element.
    if (page_is_being_dismissed) {
      new_image_content = nullptr;
    }

    ClearFailedLoadURL();
  } else {
    if (!image_source_url.IsNull()) {
      // Fire an error event if the url string is not empty, but the KURL is.
      QueuePendingErrorEvent();
    }
    NoImageResourceToLoad();
  }

  ImageResourceContent* old_image_content = image_content_.Get();
  if (old_image_content != new_image_content)
    RejectPendingDecodes(update_type);

  if (update_behavior == kUpdateSizeChanged && element_->GetLayoutObject() &&
      element_->GetLayoutObject()->IsImage() &&
      new_image_content == old_image_content) {
    To<LayoutImage>(element_->GetLayoutObject())->NaturalSizeChanged();
  } else {
    bool is_lazyload = lazy_image_load_state_ == LazyImageLoadState::kDeferred;

    // Loading didn't start (loading of images was disabled). We show fallback
    // contents here, while we don't dispatch an 'error' event etc., because
    // spec-wise the image remains in the "Unavailable" state.
    if (new_image_content &&
        new_image_content->GetContentStatus() == ResourceStatus::kNotStarted &&
        !is_lazyload)
      NoImageResourceToLoad();

    if (pending_load_event_.IsActive())
      pending_load_event_.Cancel();

    // Cancel error events that belong to the previous load, which is now
    // cancelled by changing the src attribute. If newImage is null and
    // has_pending_error_event_ is true, we know the error event has been just
    // posted by this load and we should not cancel the event.
    // FIXME: If both previous load and this one got blocked with an error, we
    // can receive one error event instead of two.
    if (pending_error_event_.IsActive() && new_image_content)
      pending_error_event_.Cancel();

    UpdateImageState(new_image_content);

    UpdateLayoutObject();
    // If newImage exists and is cached, addObserver() will result in the load
    // event being queued to fire. Ensure this happens after beforeload is
    // dispatched.
    if (new_image_content) {
      new_image_content->AddObserver(this);
      document.Fetcher()->StartSpeculativeImageDecodes();
    }
    if (old_image_content) {
      old_image_content->RemoveObserver(this);
    }
  }

  ResetAnimation(update_behavior == kUpdateForcedReload
                     ? ResetTimeline::kAll
                     : ResetTimeline::kSharedOnly);
}

void ImageLoader::UpdateFromElement(UpdateFromElementBehavior update_behavior,
                                    bool force_blocking) {
  if (!element_->GetDocument().IsActive()) {
    return;
  }

  AtomicString image_source_url = element_->ImageSourceURL();
  suppress_error_events_ = (update_behavior == kUpdateSizeChanged);

  if (update_behavior == kUpdateIgnorePreviousError)
    ClearFailedLoadURL();

  if (!failed_load_url_.empty() && image_source_url == failed_load_url_)
    return;

  // Prevent the creation of a ResourceLoader (and therefore a network request)
  // for ImageDocument loads. In this case, the image contents have already been
  // requested as a main resource and ImageDocumentParser will take care of
  // funneling the main resource bytes into |image_content_for_image_document_|,
  // so just pick up the ImageResourceContent that has been provided.
  if (image_content_for_image_document_) {
    DCHECK_NE(update_behavior, kUpdateForcedReload);
    SetImageWithoutConsideringPendingLoadEvent(
        image_content_for_image_document_);
    image_content_for_image_document_ = nullptr;
    return;
  }

  // If we have a pending task, we have to clear it -- either we're now loading
  // immediately, or we need to reset the task's state.
  if (pending_task_) {
    pending_task_->ClearLoader();
    pending_task_.reset();
    // Here we need to clear delay_until_do_update_from_element to avoid causing
    // a memory leak in case it's already created.
    delay_until_do_update_from_element_ = nullptr;
  }

  // Soft Navigation tracking needs to know about image changes caused by
  // attribute changes, e.g. changing an HTMLImageElement's src, so it can
  // attribute the subsequent paint.
  if (update_behavior == kUpdateIgnorePreviousError) {
    SoftNavigationHeuristics::ModifiedNode(element_.Get());
  }

  const KURL image_source_kurl = ImageSourceToKURL(image_source_url);
  if (ShouldLoadImmediately(image_source_kurl) &&
      update_behavior != kUpdateFromMicrotask) {
    DoUpdateFromElement(element_->GetExecutionContext()->GetCurrentWorld(),
                        update_behavior, &image_source_kurl, UpdateType::kSync,
                        force_blocking);
    return;
  }
  // Allow the idiom "img.src=''; img.src='.." to clear down the image before an
  // asynchronous load completes.
  if (image_source_url.empty()) {
    ImageResourceContent* image = image_content_.Get();
    if (image) {
      image->RemoveObserver(this);
    }
    image_content_ = nullptr;
    image_complete_ = true;
    image_content_for_image_document_ = nullptr;
    delay_until_image_notify_finished_ = nullptr;
    if (lazy_image_load_state_ != LazyImageLoadState::kNone) {
      LazyImageHelper::StopMonitoring(GetElement());
      lazy_image_load_state_ = LazyImageLoadState::kNone;
    }
  } else {
    image_complete_ = false;
  }

  // Don't load images for inactive documents or active documents without V8
  // context. We don't want to slow down the raw HTML parsing case by loading
  // images we don't intend to display.
  if (element_->GetDocument().IsActive())
    EnqueueImageLoadingMicroTask(update_behavior);
}

KURL ImageLoader::ImageSourceToKURL(AtomicString image_source_url) const {
  KURL url;

  // Don't load images for inactive documents. We don't want to slow down the
  // raw HTML parsing case by loading images we don't intend to display.
  Document& document = element_->GetDocument();
  if (!document.IsActive())
    return url;

  // Do not load any image if the 'src' attribute is missing or if it is
  // an empty string.
  if (!image_source_url.IsNull()) {
    StringView stripped_image_source_url =
        StripLeadingAndTrailingHtmlSpaces(image_source_url);
    if (!stripped_image_source_url.empty()) {
      // Resolve the URL using the originating document, which can be different
      // if this element was sourced from a (SVG <use>) resource document.
      const Document& resolver_document =
          RuntimeEnabledFeatures::SvgUseNestedResourceDocumentsEnabled()
              ? element_->OriginatingTreeScope().GetDocument()
              : document;
      url = resolver_document.CompleteURL(stripped_image_source_url);
    }
  }
  return url;
}

bool ImageLoader::ShouldLoadImmediately(const KURL& url) const {
  // We force any image loads which might require alt content through the
  // asynchronous path so that we can add the shadow DOM for the alt-text
  // content when style recalc is over and DOM mutation is allowed again.
  if (!url.IsNull()) {
    Resource* resource = MemoryCache::Get()->ResourceForURL(
        url, element_->GetDocument().Fetcher()->GetCacheIdentifier(
                 url, /*skip_service_worker=*/false));

    if (resource && !resource->ErrorOccurred() &&
        CanReuseFromListOfAvailableImages(
            resource,
            GetCrossOriginAttributeValue(
                element_->FastGetAttribute(html_names::kCrossoriginAttr)),
            element_->GetExecutionContext()->GetSecurityOrigin())) {
      return true;
    }
  }

  return (IsA<HTMLObjectElement>(*element_) ||
          IsA<HTMLEmbedElement>(*element_) || IsA<HTMLVideoElement>(*element_));
}

void ImageLoader::ImageChanged(ImageResourceContent* content,
                               CanDeferInvalidation) {
  DCHECK_EQ(content, image_content_.Get());
  if (image_complete_ || !content->IsLoading() ||
      delay_until_image_notify_finished_)
    return;

  Document& document = element_->GetDocument();
  if (!document.IsActive())
    return;

  if (lazy_image_load_state_ != LazyImageLoadState::kDeferred ||
      !RuntimeEnabledFeatures::LazyImageConformantLoadEventTimingEnabled()) {
    delay_until_image_notify_finished_ =
        std::make_unique<IncrementLoadEventDelayCount>(document);
  }
}

void ImageLoader::ImageNotifyFinished(ImageResourceContent* content) {
  RESOURCE_LOADING_DVLOG(1)
      << "ImageLoader::imageNotifyFinished " << this
      << "; has pending load event=" << pending_load_event_.IsActive();

  DCHECK(failed_load_url_.empty());
  DCHECK_EQ(content, image_content_.Get());

  CHECK(!image_complete_);

  if (lazy_image_load_state_ == LazyImageLoadState::kDeferred) {
    // Some other content may have triggered the load of this image, but that
    // shouldn't fire this <img loading="lazy">'s load event at this time,
    // because in the spec this <img> is still in Step 25 of
    // https://html.spec.whatwg.org/#update-the-image-data.
    // When LazyLoadMediaObserver reports it to be intersecting (or close to)
    // the viewport later (i.e. this <img> proceeds to Step 26 of the spec),
    // actual load/error event will be fired, by going through the loading
    // process again from `UpdateFromElement()`. Note that in Chromium
    // implementation (unlike in the spec), the image content itself can be
    // still loaded/updated even in this case, which can be observed via e.g.
    // <img>'s width/height.
    if (RuntimeEnabledFeatures::LazyImageConformantLoadEventTimingEnabled()) {
      return;
    }
    // TODO(paint-dev): This is incorrect legacy behavior: the loading="lazy"
    // <img> will fire its load event immediately. If the above feature flag
    // doesn't cause breakage this should be removed.
    LazyImageHelper::StopMonitoring(GetElement());
    lazy_image_load_state_ = LazyImageLoadState::kFullImage;
  }

  image_complete_ = true;
  delay_until_image_notify_finished_ = nullptr;

  UpdateLayoutObject();

  if (image_content_ && image_content_->HasImage()) {
    Image& image = *image_content_->GetImage();

    if (auto* svg_image = DynamicTo<SVGImage>(image)) {
      // Check that the SVGImage has completed loading (i.e the 'load' event
      // has been dispatched in the SVG document).
      svg_image->CheckLoaded();
      svg_image->UpdateUseCountersAfterLoad(GetElement()->GetDocument());
    }
  }

  DispatchDecodeRequestsIfComplete();

  if (content->ErrorOccurred()) {
    pending_load_event_.Cancel();

    std::optional<ResourceError> error = content->GetResourceError();
    if (error && error->IsAccessCheck())
      CrossSiteOrCSPViolationOccurred(AtomicString(error->FailingURL()));

    QueuePendingErrorEvent();
    return;
  }

  content->RecordDecodedImageType(&element_->GetDocument());
  content->RecordDecodedImageC2PA(&element_->GetDocument());

  CHECK(!pending_load_event_.IsActive());
  pending_load_event_ = PostCancellableTask(
      *GetElement()->GetDocument().GetTaskRunner(TaskType::kDOMManipulation),
      FROM_HERE,
      BindOnce(&ImageLoader::DispatchPendingLoadEvent, WrapPersistent(this),
               std::make_unique<IncrementLoadEventDelayCount>(
                   GetElement()->GetDocument())));
}

LayoutImageResource* ImageLoader::GetLayoutImageResource() const {
  LayoutObject* layout_object = element_->GetLayoutObject();

  if (!layout_object)
    return nullptr;

  // We don't return style generated image because it doesn't belong to the
  // ImageLoader. See <https://bugs.webkit.org/show_bug.cgi?id=42840>
  if (layout_object->IsImage() &&
      !To<LayoutImage>(layout_object)->IsGeneratedContent())
    return To<LayoutImage>(layout_object)->ImageResource();

  if (layout_object->IsSVGImage())
    return To<LayoutSVGImage>(layout_object)->ImageResource();

  if (auto* layout_video = DynamicTo<LayoutVideo>(layout_object))
    return layout_video->ImageResource();

  return nullptr;
}

void ImageLoader::OnAttachLayoutTree() {
  LayoutImageResource* image_resource = GetLayoutImageResource();
  if (!image_resource) {
    return;
  }
  // If the LayoutImageResource already has an image, it either means that it
  // hasn't been freshly created or that it is generated content ("content:
  // url(...)") - in which case we don't need to do anything or shouldn't do
  // anything respectively.
  if (image_resource->HasImage()) {
    return;
  }
  image_resource->SetImageResource(image_content_);
}

void ImageLoader::ResetAnimation(ResetTimeline timeline) {
  bool is_svg = image_content_ && image_content_->HasImage() &&
                image_content_->GetImage()->IsSVGImage();

  if (!RuntimeEnabledFeatures::SvgImageAnimationResetEnabled() || !is_svg) {
    if (LayoutImageResource* image_resource = GetLayoutImageResource()) {
      image_resource->ResetAnimation(timeline);
    }
    return;
  }

  if (auto* image = DynamicTo<BitmapImage>(image_content_->GetImage());
      image && timeline == ImageLoader::ResetTimeline::kSharedOnly) {
    image->ResetAnimationSharedTimelineOnly();
  } else {
    image_content_->GetImage()->ResetAnimation();
  }

  if (LayoutImageResource* image_resource = GetLayoutImageResource();
      image_resource && image_resource->CachedImage() == image_content_) {
    image_resource->InvalidatePaint();
  }
}

void ImageLoader::UpdateLayoutObject() {
  LayoutImageResource* image_resource = GetLayoutImageResource();

  if (!image_resource)
    return;

  // Only update the layoutObject if it doesn't have an image or if what we have
  // is a complete image.  This prevents flickering in the case where a dynamic
  // change is happening between two images.
  ImageResourceContent* cached_image_content = image_resource->CachedImage();
  if (image_content_ != cached_image_content &&
      (image_complete_ || !cached_image_content))
    image_resource->SetImageResource(image_content_.Get());
}

gfx::Size ImageLoader::DensityCorrectedNaturalSize(
    float inverse_density) const {
  if (!image_content_ || !image_content_->HasImage() ||
      image_content_->ErrorOccurred()) {
    return gfx::Size();
  }
  Image& image = *image_content_->GetImage();
  if (auto* svg_image = DynamicTo<SVGImage>(image)) {
    std::optional<NaturalSizingInfo> sizing_info =
        SVGImageForContainer::GetNaturalDimensions(*svg_image, nullptr);
    if (!sizing_info) {
      return gfx::Size();
    }
    sizing_info->size.Scale(inverse_density);

    static constexpr gfx::SizeF kDefaultObjectSize{300, 150};
    return ToRoundedSize(PhysicalSize::FromSizeFFloor(
        blink::ConcreteObjectSize(*sizing_info, kDefaultObjectSize)));
  }
  PhysicalSize natural_size(image.Size(kRespectImageOrientation));
  natural_size.Scale(inverse_density);
  return {natural_size.width.ToInt(), natural_size.height.ToInt()};
}

ResourcePriority ImageLoader::ComputeResourcePriority() const {
  LayoutImageResource* image_resource = GetLayoutImageResource();
  if (!image_resource)
    return ResourcePriority();

  ResourcePriority priority = image_resource->ComputeResourcePriority();
  priority.source = ResourcePriority::Source::kImageLoader;

  static const bool is_image_lcpp_enabled =
      base::FeatureList::IsEnabled(features::kLCPCriticalPathPredictor) &&
      features::
          kLCPCriticalPathPredictorImageLoadPriorityEnabledForHTMLImageElement
              .Get();
  if (is_image_lcpp_enabled) {
    if (auto* html_image_element =
            DynamicTo<HTMLImageElement>(element_.Get())) {
      priority.is_lcp_resource = html_image_element->IsPredictedLcpElement();
    }
  }
  return priority;
}

bool ImageLoader::HasPendingEvent() const {
  // Regular image loading is in progress.
  if (image_content_ && !image_complete_ &&
      lazy_image_load_state_ != LazyImageLoadState::kDeferred) {
    return true;
  }

  if (pending_load_event_.IsActive() || pending_error_event_.IsActive() ||
      !decode_requests_.empty()) {
    return true;
  }

  return false;
}

void ImageLoader::DispatchPendingLoadEvent(
    std::unique_ptr<IncrementLoadEventDelayCount> count) {
  if (!image_content_)
    return;
  CHECK(image_complete_);
  DispatchLoadEvent();

  // Checks Document's load event synchronously here for performance.
  // This is safe because DispatchPendingLoadEvent() is called asynchronously.
  count->ClearAndCheckLoadEvent();
}

void ImageLoader::DispatchPendingErrorEvent(
    std::unique_ptr<IncrementLoadEventDelayCount> count) {
  DispatchErrorEvent();

  // Checks Document's load event synchronously here for performance.
  // This is safe because DispatchPendingErrorEvent() is called asynchronously.
  count->ClearAndCheckLoadEvent();
}

bool ImageLoader::GetImageAnimationPolicy(
    mojom::blink::ImageAnimationPolicy& policy) {
  if (!GetElement()->GetDocument().GetSettings())
    return false;

  policy = GetElement()->GetDocument().GetSettings()->GetImageAnimationPolicy();
  return true;
}

ScriptPromise<IDLUndefined> ImageLoader::Decode(
    ScriptState* script_state,
    ExceptionState& exception_state) {
  ExecutionContext* execution_context = ExecutionContext::From(script_state);
  // It's possible that |script_state|'s context isn't valid, which means we
  // should immediately reject the request. This is possible in situations like
  // the document that created this image was already destroyed (like an img
  // that comes from iframe.contentDocument.createElement("img") and the iframe
  // is destroyed).
  if (!script_state->ContextIsValid() || !execution_context) {
    exception_state.ThrowDOMException(DOMExceptionCode::kEncodingError,
                                      "The source image cannot be decoded.");
    return EmptyPromise();
  }

  UseCounter::Count(execution_context, WebFeature::kImageDecodeAPI);

  auto* request = MakeGarbageCollected<DecodeRequest>(
      this, MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
                script_state, exception_state.GetContext()));
  execution_context->GetAgent()->event_loop()->EnqueueMicrotask(
      BindOnce(&DecodeRequest::ProcessForTask, WrapWeakPersistent(request)));
  decode_requests_.push_back(request);
  return request->promise();
}

void ImageLoader::LoadDeferredImage(bool force_blocking,
                                    bool update_from_microtask) {
  if (lazy_image_load_state_ != LazyImageLoadState::kDeferred)
    return;
  DCHECK(!image_complete_);
  lazy_image_load_state_ = LazyImageLoadState::kFullImage;

  // If the image has been fully deferred (no placeholder fetch), report it as
  // fully loaded now.
  UpdateFromElement(
      update_from_microtask ? kUpdateFromMicrotask : kUpdateNormal,
      force_blocking);
}

void ImageLoader::ElementDidMoveToNewDocument() {
  if (delay_until_do_update_from_element_) {
    delay_until_do_update_from_element_->DocumentChanged(
        element_->GetDocument());
  }
  if (delay_until_image_notify_finished_) {
    delay_until_image_notify_finished_->DocumentChanged(
        element_->GetDocument());
  }
  ClearFailedLoadURL();
  ClearImage();
}

// Indicates the next available id that we can use to uniquely identify a decode
// request.
uint64_t ImageLoader::DecodeRequest::s_next_request_id_ = 0;

ImageLoader::DecodeRequest::DecodeRequest(
    ImageLoader* loader,
    ScriptPromiseResolver<IDLUndefined>* resolver)
    : request_id_(s_next_request_id_++), resolver_(resolver), loader_(loader) {}

void ImageLoader::DecodeRequest::Resolve() {
  resolver_->Resolve();
  loader_ = nullptr;
}

void ImageLoader::DecodeRequest::Reject() {
  resolver_->Reject(MakeGarbageCollected<DOMException>(
      DOMExceptionCode::kEncodingError, "The source image cannot be decoded."));
  loader_ = nullptr;
}

void ImageLoader::DecodeRequest::ProcessForTask() {
  // We could have already processed (ie rejected) this task due to a sync
  // update in UpdateFromElement. In that case, there's nothing to do here.
  if (!loader_)
    return;

  DCHECK_EQ(state_, kPendingMicrotask);
  state_ = kPendingLoad;
  loader_->DispatchDecodeRequestsIfComplete();
}

void ImageLoader::DecodeRequest::NotifyDecodeDispatched() {
  DCHECK_EQ(state_, kPendingLoad);
  state_ = kDispatched;
}

void ImageLoader::DecodeRequest::Trace(Visitor* visitor) const {
  visitor->Trace(resolver_);
  visitor->Trace(loader_);
}

}  // namespace blink
