// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/core/geolocation/geo_notifier.h"

#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_position_options.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/geolocation/geolocation.h"
#include "third_party/blink/renderer/core/geolocation/geolocation_position_error.h"

namespace blink {

GeoNotifier::GeoNotifier(Geolocation* geolocation,
                         const PositionOptions* options)
    : geolocation_(geolocation),
      options_(options),
      timer_(MakeGarbageCollected<Timer>(
          geolocation->DomWindow()->GetTaskRunner(TaskType::kMiscPlatformAPI),
          this,
          &GeoNotifier::TimerFired)),
      use_cached_position_(false),
      initial_callback_run_(false),
      remaining_timeout_(std::nullopt) {
  DCHECK(geolocation_);
}

void GeoNotifier::Trace(Visitor* visitor) const {
  visitor->Trace(geolocation_);
  visitor->Trace(options_);
  visitor->Trace(timer_);
  visitor->Trace(fatal_error_);
}

void GeoNotifier::SetFatalError(GeolocationPositionError* error) {
  // If a fatal error has already been set, stick with it. This makes sure that
  // when permission is denied, this is the error reported, as required by the
  // spec.
  if (fatal_error_)
    return;

  fatal_error_ = error;
  // An existing timer may not have a zero timeout.
  timer_->Stop();
  timer_->StartOneShot(base::TimeDelta(), FROM_HERE);
}

void GeoNotifier::SetUseCachedPosition() {
  use_cached_position_ = true;
  timer_->StartOneShot(base::TimeDelta(), FROM_HERE);
}

void GeoNotifier::RunSuccessCallback(Geoposition* position) {
  initial_callback_run_ = true;
  LocalDOMWindow* win = geolocation_->DomWindow();
  UseCounter::Count(win, WebFeature::kGeolocationSucceeded);

  if (!win->IsInjectionMitigatedContext()) {
    UseCounter::Count(
        win, WebFeature::kGeolocationSucceededWithoutInjectionMitigation);
  }
  RunCallback(position, nullptr);
}

void GeoNotifier::RunErrorCallback(GeolocationPositionError* error) {
  initial_callback_run_ = true;
  RunCallback(nullptr, error);
}

void GeoNotifier::StartTimer() {
  base::TimeDelta timeout =
      remaining_timeout_.value_or(base::Milliseconds(options_->timeout()));
  remaining_timeout_ = std::nullopt;
  timer_->StartOneShot(timeout, FROM_HERE);
}

void GeoNotifier::StopTimer() {
  if (timer_->IsActive()) {
    remaining_timeout_ = timer_->RemainingTimeout();
    timer_->Stop();
  }
}

bool GeoNotifier::IsTimerActive() const {
  return timer_->IsActive();
}

void GeoNotifier::Timer::Trace(Visitor* visitor) const {
  visitor->Trace(timer_);
  visitor->Trace(notifier_);
}

void GeoNotifier::Timer::StartOneShot(base::TimeDelta interval,
                                      const base::Location& caller) {
  DCHECK(notifier_->geolocation_->DoesOwnNotifier(notifier_));
  timer_.StartOneShot(interval, caller);
}

void GeoNotifier::Timer::Stop() {
  DCHECK(notifier_->geolocation_->DoesOwnNotifier(notifier_));
  timer_.Stop();
}

base::TimeDelta GeoNotifier::Timer::RemainingTimeout() const {
  DCHECK(notifier_->geolocation_->DoesOwnNotifier(notifier_));
  return timer_.NextFireInterval();
}

void GeoNotifier::TimerFired(TimerBase*) {
  timer_->Stop();

  // As the timer fires asynchronously, it's possible that the execution context
  // has already gone.  Check it first.
  if (!geolocation_->GetExecutionContext()) {
    return;  // Do not invoke anything because of no execution context.
  }
  // TODO(yukishiino): Remove this check once we understand the cause.
  // https://crbug.com/792604
  CHECK(!geolocation_->GetExecutionContext()->IsContextDestroyed());
  CHECK(geolocation_->DoesOwnNotifier(this));

  // Test for fatal error first. This is required for the case where the
  // LocalFrame is disconnected and requests are cancelled.
  if (fatal_error_) {
    RunErrorCallback(fatal_error_);
    // This will cause this notifier to be deleted.
    geolocation_->FatalErrorOccurred(this);
    return;
  }

  if (use_cached_position_) {
    // Clear the cached position flag in case this is a watch request, which
    // will continue to run.
    use_cached_position_ = false;
    geolocation_->RequestUsesCachedPosition(this);
    return;
  }

  RunCallback(nullptr,
              MakeGarbageCollected<GeolocationPositionError>(
                  GeolocationPositionError::kTimeout, "Timeout expired"));

  geolocation_->RequestTimedOut(this);
}

}  // namespace blink
