/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "StreamFilter.h"

#include "jsapi.h"
#include "jsfriendapi.h"
#include "xpcpublic.h"

#include "mozilla/AbstractThread.h"
#include "mozilla/extensions/StreamFilterChild.h"
#include "mozilla/extensions/StreamFilterEvents.h"
#include "mozilla/extensions/StreamFilterParent.h"
#include "mozilla/dom/AutoEntryScript.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/ipc/Endpoint.h"
#include "nsContentUtils.h"
#include "nsCycleCollectionParticipant.h"
#include "nsLiteralString.h"
#include "nsThreadUtils.h"
#include "nsTArray.h"

using namespace JS;
using namespace mozilla::dom;

namespace mozilla {
namespace extensions {

/*****************************************************************************
 * Initialization
 *****************************************************************************/

StreamFilter::StreamFilter(nsIGlobalObject* aParent, uint64_t aRequestId,
                           const nsAString& aAddonId)
    : mParent(aParent), mChannelId(aRequestId), mAddonId(NS_Atomize(aAddonId)) {
  MOZ_ASSERT(aParent);

  Connect();
};

StreamFilter::~StreamFilter() { ForgetActor(); }

void StreamFilter::ForgetActor() {
  if (mActor) {
    mActor->Cleanup();
    mActor->SetStreamFilter(nullptr);
  }
}

/* static */
already_AddRefed<StreamFilter> StreamFilter::Create(GlobalObject& aGlobal,
                                                    uint64_t aRequestId,
                                                    const nsAString& aAddonId) {
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  MOZ_ASSERT(global);

  RefPtr<StreamFilter> filter = new StreamFilter(global, aRequestId, aAddonId);
  return filter.forget();
}

/*****************************************************************************
 * Actor allocation
 *****************************************************************************/

void StreamFilter::Connect() {
  MOZ_ASSERT(!mActor);

  mActor = new StreamFilterChild();
  mActor->SetStreamFilter(this);

  nsAutoString addonId;
  mAddonId->ToString(addonId);

  ContentChild* cc = ContentChild::GetSingleton();
  RefPtr<StreamFilter> self(this);
  if (cc) {
    cc->SendInitStreamFilter(mChannelId, addonId)
        ->Then(
            GetCurrentSerialEventTarget(), __func__,
            [self](mozilla::ipc::Endpoint<PStreamFilterChild>&& aEndpoint) {
              self->FinishConnect(std::move(aEndpoint));
            },
            [self](mozilla::ipc::ResponseRejectReason&& aReason) {
              self->mActor->RecvInitialized(false);
            });
  } else {
    StreamFilterParent::Create(nullptr, mChannelId, addonId)
        ->Then(
            GetCurrentSerialEventTarget(), __func__,
            [self](mozilla::ipc::Endpoint<PStreamFilterChild>&& aEndpoint) {
              self->FinishConnect(std::move(aEndpoint));
            },
            [self](bool aDummy) { self->mActor->RecvInitialized(false); });
  }
}

void StreamFilter::FinishConnect(
    mozilla::ipc::Endpoint<PStreamFilterChild>&& aEndpoint) {
  if (aEndpoint.IsValid()) {
    MOZ_RELEASE_ASSERT(aEndpoint.Bind(mActor));
    mActor->RecvInitialized(true);
  } else {
    mActor->RecvInitialized(false);
  }
}

bool StreamFilter::CheckAlive() {
  // Check whether the global that owns this StreamFitler is still scriptable
  // and, if not, disconnect the actor so that it can be cleaned up.
  JSObject* wrapper = GetWrapperPreserveColor();
  if (!wrapper || !xpc::Scriptability::Get(wrapper).Allowed()) {
    ForgetActor();
    return false;
  }
  return true;
}

/*****************************************************************************
 * Binding methods
 *****************************************************************************/

void StreamFilter::Write(const ArrayBufferOrUint8Array& aData,
                         ErrorResult& aRv) {
  if (!mActor) {
    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
    return;
  }

  nsTArray<uint8_t> data;
  if (!AppendTypedArrayDataTo(aData, data)) {
    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  mActor->Write(std::move(data), aRv);
}

StreamFilterStatus StreamFilter::Status() const {
  if (!mActor) {
    return StreamFilterStatus::Uninitialized;
  }
  return mActor->Status();
}

void StreamFilter::Suspend(ErrorResult& aRv) {
  if (mActor) {
    mActor->Suspend(aRv);
  } else {
    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
  }
}

void StreamFilter::Resume(ErrorResult& aRv) {
  if (mActor) {
    mActor->Resume(aRv);
  } else {
    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
  }
}

void StreamFilter::Disconnect(ErrorResult& aRv) {
  if (mActor) {
    mActor->Disconnect(aRv);
  } else {
    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
  }
}

void StreamFilter::Close(ErrorResult& aRv) {
  if (mActor) {
    mActor->Close(aRv);
  } else {
    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
  }
}

/*****************************************************************************
 * Event emitters
 *****************************************************************************/

void StreamFilter::FireEvent(const nsAString& aType) {
  EventInit init;
  init.mBubbles = false;
  init.mCancelable = false;

  RefPtr<Event> event = Event::Constructor(this, aType, init);
  event->SetTrusted(true);

  DispatchEvent(*event);
}

void StreamFilter::FireDataEvent(const nsTArray<uint8_t>& aData) {
  AutoEntryScript aes(mParent, "StreamFilter data event");
  JSContext* cx = aes.cx();

  RootedDictionary<StreamFilterDataEventInit> init(cx);
  init.mBubbles = false;
  init.mCancelable = false;

  // TODO: There is no way to recover from this. This chunk of data is lost.
  auto* buffer = ArrayBuffer::Create(cx, aData, IgnoreErrors());
  if (!buffer) {
    FireErrorEvent(u"Out of memory"_ns);
    return;
  }

  init.mData.Init(buffer);

  RefPtr<StreamFilterDataEvent> event =
      StreamFilterDataEvent::Constructor(this, u"data"_ns, init);
  event->SetTrusted(true);

  DispatchEvent(*event);
}

void StreamFilter::FireErrorEvent(const nsAString& aError) {
  MOZ_ASSERT(mError.IsEmpty());

  mError = aError;
  FireEvent(u"error"_ns);
}

/*****************************************************************************
 * Glue
 *****************************************************************************/

/* static */
bool StreamFilter::IsAllowedInContext(JSContext* aCx, JSObject* /* unused */) {
  return nsContentUtils::CallerHasPermission(aCx,
                                             nsGkAtoms::webRequestBlocking);
}

JSObject* StreamFilter::WrapObject(JSContext* aCx,
                                   JS::Handle<JSObject*> aGivenProto) {
  return StreamFilter_Binding::Wrap(aCx, this, aGivenProto);
}

NS_IMPL_CYCLE_COLLECTION_CLASS(StreamFilter)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StreamFilter)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(StreamFilter,
                                                DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(StreamFilter,
                                                  DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(StreamFilter,
                                               DOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_TRACE_END

NS_IMPL_ADDREF_INHERITED(StreamFilter, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(StreamFilter, DOMEventTargetHelper)

}  // namespace extensions
}  // namespace mozilla
