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

#include <list>

#include "base/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/sequence_checker.h"
#include "base/synchronization/lock.h"
#include "base/task/post_task.h"
#include "base/task/sequenced_task_runner_forward.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "mojo/public/cpp/bindings/lib/binding_state.h"
#include "mojo/public/cpp/bindings/lib/test_random_mojo_delays.h"

namespace mojo {
namespace internal {

namespace {
constexpr int kInverseProbabilityOfDelay = 8;
constexpr int kInverseProbabilityOfNotResuming = 10;
constexpr base::TimeDelta kMillisecondsToResume = base::Milliseconds(2);
constexpr base::TimeDelta kPauseBindingsFrequency = base::Milliseconds(7);
}  // namespace

// TODO(mpdenton) This only adds random delays on method call processing. This
// also should add random delays on response processing. It is a mistake if a
// user assumes a response callback is received and run before a subsequent
// asynch call (over a different message pipe), and these random delays won't
// make it any more likely to find the mistake during testing.
class RandomMojoDelays {
 public:
  RandomMojoDelays()
      : runner_for_pauses_(base::ThreadPool::CreateSequencedTaskRunner({})) {
    DETACH_FROM_SEQUENCE(runner_for_pauses_sequence_checker);
  }

  void Start() {
    runner_for_pauses_->PostTask(
        FROM_HERE,
        base::BindOnce(&RandomMojoDelays::PauseRandomBindingStateBases,
                       base::Unretained(this)));
  }

  // TODO(mpdenton) what about bindings with associated interfaces? Apparently
  // you cannot pause on those? May need to change DCHECK to if(...) return;
  void AddBindingStateBase(scoped_refptr<base::SequencedTaskRunner> runner,
                           base::WeakPtr<BindingStateBase> binding_state_base) {
    runner_for_pauses_->PostTask(
        FROM_HERE,
        base::BindOnce(&RandomMojoDelays::AddBindingStateBaseInternal,
                       base::Unretained(this), std::move(runner),
                       std::move(binding_state_base)));
  }

 private:
  using BindingList = std::list<base::WeakPtr<BindingStateBase>>;

  // Must be called on |runner_for_pauses_| sequence. Adds a BindingStateBase
  // for random pausing purposes.
  void AddBindingStateBaseInternal(
      scoped_refptr<base::SequencedTaskRunner> runner,
      base::WeakPtr<BindingStateBase> binding_state_base) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(runner_for_pauses_sequence_checker);

    binding_state_base_map_[std::move(runner)].push_back(
        std::move(binding_state_base));
  }

  // Adds a list of BindingStateBases to be randomly paused. Used to re-attach
  // a list of BindingStateBases to the map after randomly pausing some of the
  // bindings on their bound sequences.
  void AddBindingStateBaseList(scoped_refptr<base::SequencedTaskRunner> runner,
                               BindingList binding_state_bases) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(runner_for_pauses_sequence_checker);

    BindingList& list = binding_state_base_map_[std::move(runner)];
    list.splice(list.end(), std::move(binding_state_bases));
  }

  // Resumes all bindings in |paused_binding_state_bases|.
  void ResumeFrozenBindingStateBasesOnTaskRunner(
      BindingList binding_state_bases,
      BindingList paused_binding_state_bases) {
    auto it = paused_binding_state_bases.begin();
    while (it != paused_binding_state_bases.end()) {
      base::WeakPtr<BindingStateBase> wptr = *it;
      if (!wptr) {
        // This WeakPtr was invalidated. We'll delete it
        // from the binding_state_bases list on the next PauseLoop.
        it = paused_binding_state_bases.erase(it);
        continue;
      }
      // Skip the resume with a 1/kInverseProbabilityOfNotResuming chance.
      if (base::RandInt(1, kInverseProbabilityOfNotResuming) >= 2) {
        wptr->ResumeIncomingMethodCallProcessing();
        it = paused_binding_state_bases.erase(it);
        continue;
      }
      it++;
    }
    if (!paused_binding_state_bases.empty()) {
      // Because we haven't resumed all the bindings, we should schedule another
      // resumption task in the future.
      // TODO(mpdenton) similar problem as below: can freeze shutdown if we
      // forget to unpause bindings.
      base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(
              &RandomMojoDelays::ResumeFrozenBindingStateBasesOnTaskRunner,
              base::Unretained(this), std::move(binding_state_bases),
              std::move(paused_binding_state_bases)),
          kMillisecondsToResume);
      return;
    }
    // Re-attach the bindings to the global map for future pausing.
    runner_for_pauses_->PostTask(
        FROM_HERE, base::BindOnce(&RandomMojoDelays::AddBindingStateBaseList,
                                  base::Unretained(this),
                                  base::SequencedTaskRunnerHandle::Get(),
                                  std::move(binding_state_bases)));
  }

  // Pause a random selection of bindings in the list |binding_state_bases|,
  // and set them to resume in the future.
  void PauseRandomBindingStateBasesOnTaskRunner(
      BindingList binding_state_bases) {
    BindingList paused_binding_state_bases;
    auto it = binding_state_bases.begin();
    while (it != binding_state_bases.end()) {
      // Remove any BindingStateBases that have been destroyed already.
      base::WeakPtr<BindingStateBase> wptr = *it;
      if (!wptr) {
        it = binding_state_bases.erase(it);
        continue;
      }
      if (base::RandInt(1, kInverseProbabilityOfDelay) >= 2) {
        it++;
        continue;
      }
      wptr->PauseIncomingMethodCallProcessing();
      paused_binding_state_bases.push_back(wptr);
      it++;
    }
    // Set the bindings to resume soon.
    // TODO(mpdenton) may cause deadlock on shutdown if this doesn't run. But
    // there is no PostDelayedTask for a SequencedTaskRunner.
    if (paused_binding_state_bases.size() > 0) {
      base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(
              &RandomMojoDelays::ResumeFrozenBindingStateBasesOnTaskRunner,
              base::Unretained(this), std::move(binding_state_bases),
              std::move(paused_binding_state_bases)),
          kMillisecondsToResume);
    } else if (binding_state_bases.size() > 0) {
      // If we did not pause any bindings, re-attach the bindings to the global
      // map for future pausing, if there are any left after deleting all the
      // invalidated weak ptrs.
      runner_for_pauses_->PostTask(
          FROM_HERE, base::BindOnce(&RandomMojoDelays::AddBindingStateBaseList,
                                    base::Unretained(this),
                                    base::SequencedTaskRunnerHandle::Get(),
                                    std::move(binding_state_bases)));
    }
  }

  // Post tasks to every sequence with bound Mojo bindings, telling each to
  // pause random Mojo bindings bound on the respective sequence.
  void PauseRandomBindingStateBases() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(runner_for_pauses_sequence_checker);

    auto map_it = binding_state_base_map_.begin();
    while (map_it != binding_state_base_map_.end()) {
      // Tell sequence to randomly pause some of its bindings.
      map_it->first->PostTask(
          FROM_HERE,
          base::BindOnce(
              &RandomMojoDelays::PauseRandomBindingStateBasesOnTaskRunner,
              base::Unretained(this), std::move(map_it->second)));
      // Erase the current key-value pair (it will be re-added if necessary
      // after resuming the bindings--for now, drop the reference to the
      // SequencedTaskRunner).
      map_it = binding_state_base_map_.erase(map_it);
    }
    // Post delayed task, instead of using a RepeatingTimer, to avoid
    // overwhelming the task scheduling.
    base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&RandomMojoDelays::PauseRandomBindingStateBases,
                       base::Unretained(this)),
        kPauseBindingsFrequency);
  }

  scoped_refptr<base::SequencedTaskRunner> runner_for_pauses_;
  std::map<scoped_refptr<base::SequencedTaskRunner>, BindingList>
      binding_state_base_map_;
  SEQUENCE_CHECKER(runner_for_pauses_sequence_checker);
};

RandomMojoDelays& GetRandomMojoDelays() {
  static base::NoDestructor<RandomMojoDelays> random_mojo_delays;
  return *random_mojo_delays;
}

void MakeBindingRandomlyPaused(
    scoped_refptr<base::SequencedTaskRunner> runner,
    base::WeakPtr<BindingStateBase> binding_state_base) {
  GetRandomMojoDelays().AddBindingStateBase(std::move(runner),
                                            binding_state_base);
}

}  // namespace internal

void BeginRandomMojoDelays() {
  internal::GetRandomMojoDelays().Start();
}

}  // namespace mojo
