// Copyright (c) Facebook, Inc. and its affiliates.
//
// Licensed 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <atomic>
#include <memory>
#include <utility>

#include <folly/Conv.h>
#include <folly/ScopeGuard.h>

#include "yarpl/flowable/Flowable.h"
#include "yarpl/flowable/Subscriber.h"
#include "yarpl/utils/credits.h"

namespace yarpl {
namespace flowable {
namespace details {

template <typename T>
class EmiterBase {
 public:
  virtual ~EmiterBase() = default;

  virtual std::tuple<int64_t, bool> emit(std::shared_ptr<Subscriber<T>>,
                                         int64_t) = 0;
};

/**
 * Manager for a flowable subscription.
 *
 * This is synchronous: the emit calls are triggered within the context
 * of a request(n) call.
 */
template <typename T>
class EmiterSubscription final : public Subscription,
                                 public Subscriber<T>,
                                 public yarpl::enable_get_ref {
  constexpr static auto kCanceled = credits::kCanceled;
  constexpr static auto kNoFlowControl = credits::kNoFlowControl;

 public:
  EmiterSubscription(
      std::shared_ptr<EmiterBase<T>> emiter,
      std::shared_ptr<Subscriber<T>> subscriber)
      : emiter_(std::move(emiter)), subscriber_(std::move(subscriber)) {}

  void init() {
    subscriber_->onSubscribe(this->ref_from_this(this));
  }

  virtual ~EmiterSubscription() {
    subscriber_.reset();
  }

  void request(int64_t delta) override {
    while (true) {
      auto current = requested_.load(std::memory_order_relaxed);

      if (current == kCanceled) {
        // this can happen because there could be an async barrier between the
        // subscriber and the subscription for instance while onComplete is
        // being delivered (on effectively cancelled subscription) the
        // subscriber can call request(n)
        return;
      }

      auto const total = credits::add(current, delta);
      if (requested_.compare_exchange_strong(current, total)) {
        break;
      }
    }

    process();
  }

  void cancel() override {
    // if this is the first terminating signal to receive, we need to
    // make sure we break the reference cycle between subscription and
    // subscriber
    auto previous = requested_.exchange(kCanceled, std::memory_order_relaxed);
    if (previous != kCanceled) {
      // this can happen because there could be an async barrier between the
      // subscriber and the subscription for instance while onComplete is being
      // delivered (on effectively cancelled subscription) the subscriber can
      // call request(n)
      process();
    }
  }

  // Subscriber methods.
  void onSubscribe(std::shared_ptr<Subscription>) override {
    LOG(FATAL) << "Do not call this method";
  }

  void onNext(T value) override {
#ifndef NDEBUG
    DCHECK(!hasFinished_) << "onComplete() or onError() already called";
#endif
    subscriber_->onNext(std::move(value));
  }

  void onComplete() override {
#ifndef NDEBUG
    DCHECK(!hasFinished_) << "onComplete() or onError() already called";
    hasFinished_ = true;
#endif
    subscriber_->onComplete();
  }

  void onError(folly::exception_wrapper error) override {
#ifndef NDEBUG
    DCHECK(!hasFinished_) << "onComplete() or onError() already called";
    hasFinished_ = true;
#endif
    subscriber_->onError(error);
  }

 private:
  // Processing loop.  Note: this can delete `this` upon completion,
  // error, or cancellation; thus, no fields should be accessed once
  // this method returns.
  //
  // Thread-Safety: there is no guarantee as to which thread this is
  // invoked on.  However, there is a strong guarantee on cancel and
  // request(n) calls: no more than one instance of either of these
  // can be outstanding at any time.
  void process() {
    // Guards against re-entrancy in request(n) calls.
    if (processing_.exchange(true)) {
      return;
    }

    auto guard = folly::makeGuard([this] { processing_ = false; });

    // Keep a reference to ourselves here in case the emit() call
    // frees all other references to 'this'
    auto this_subscriber = this->ref_from_this(this);

    while (true) {
      auto current = requested_.load(std::memory_order_relaxed);

      // Subscription was canceled, completed, or had an error.
      if (current == kCanceled) {
        guard.dismiss();
        release();
        return;
      }

      // If no more items can be emitted now, wait for a request(n).
      // See note above re: thread-safety.  We are guaranteed that
      // request(n) is not simultaneously invoked on another thread.
      if (current <= 0)
        return;

      int64_t emitted;
      bool done;

      std::tie(emitted, done) = emiter_->emit(this_subscriber, current);

      while (true) {
        current = requested_.load(std::memory_order_relaxed);
        if (current == kCanceled || (current == kNoFlowControl && !done)) {
          break;
        }

        auto updated = done ? kCanceled : current - emitted;
        if (requested_.compare_exchange_strong(current, updated)) {
          break;
        }
      }
    }
  }

  void release() {
    emiter_.reset();
    subscriber_.reset();
  }

  // The number of items that can be sent downstream.  Each request(n)
  // adds n; each onNext consumes 1.  If this is MAX, flow-control is
  // disabled: items sent downstream don't consume any longer.  A MIN
  // value represents cancellation.  Other -ve values aren't permitted.
  std::atomic_int_fast64_t requested_{0};

#ifndef NDEBUG
  bool hasFinished_{false}; // onComplete or onError called
#endif

  // We don't want to recursively invoke process(); one loop should do.
  std::atomic_bool processing_{false};

  std::shared_ptr<EmiterBase<T>> emiter_;
  std::shared_ptr<Subscriber<T>> subscriber_;
};

template <typename T>
class TrackingSubscriber : public Subscriber<T> {
 public:
  TrackingSubscriber(
      Subscriber<T>& subscriber,
      int64_t
#ifndef NDEBUG
          requested
#endif
      )
      : inner_(&subscriber)
#ifndef NDEBUG
        ,
        requested_(requested)
#endif
  {
  }

  void onSubscribe(std::shared_ptr<Subscription> s) override {
    inner_->onSubscribe(std::move(s));
  }

  void onComplete() override {
    completed_ = true;
    inner_->onComplete();
  }

  void onError(folly::exception_wrapper ex) override {
    completed_ = true;
    inner_->onError(std::move(ex));
  }

  void onNext(T value) override {
#ifndef NDEBUG
    auto old = requested_;
    DCHECK(old > credits::consume(requested_, 1))
        << "cannot emit more than requested";
#endif
    emitted_++;
    inner_->onNext(std::move(value));
  }

  auto getResult() {
    return std::make_tuple(emitted_, completed_);
  }

 private:
  int64_t emitted_{0};
  bool completed_{false};
  Subscriber<T>* inner_;
#ifndef NDEBUG
  int64_t requested_;
#endif
};

template <typename T, typename Emitter>
class EmitterWrapper : public EmiterBase<T>, public Flowable<T> {
  static_assert(
      std::is_same<std::decay_t<Emitter>, Emitter>::value,
      "undecayed");

 public:
  template <typename F>
  explicit EmitterWrapper(F&& emitter) : emitter_(std::forward<F>(emitter)) {}

  void subscribe(std::shared_ptr<Subscriber<T>> subscriber) override {
    auto ef = std::make_shared<EmiterSubscription<T>>(
        this->ref_from_this(this), std::move(subscriber));
    ef->init();
  }

  std::tuple<int64_t, bool> emit(std::shared_ptr<Subscriber<T>> subscriber,
                                 int64_t requested) override {
    TrackingSubscriber<T> trackingSubscriber(*subscriber, requested);
    emitter_(trackingSubscriber, requested);
    return trackingSubscriber.getResult();
  }

 private:
  Emitter emitter_;
};

} // namespace details
} // namespace flowable
} // namespace yarpl
