/* 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/. */

"use strict";

const { Actor } = require("resource://devtools/shared/protocol.js");
const {
  serviceWorkerRegistrationSpec,
} = require("resource://devtools/shared/specs/worker/service-worker-registration.js");

const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs",
  { global: "contextual" }
);
const {
  PushSubscriptionActor,
} = require("resource://devtools/server/actors/worker/push-subscription.js");
const {
  ServiceWorkerActor,
} = require("resource://devtools/server/actors/worker/service-worker.js");

XPCOMUtils.defineLazyServiceGetter(
  this,
  "swm",
  "@mozilla.org/serviceworkers/manager;1",
  "nsIServiceWorkerManager"
);

XPCOMUtils.defineLazyServiceGetter(
  this,
  "PushService",
  "@mozilla.org/push/Service;1",
  "nsIPushService"
);

class ServiceWorkerRegistrationActor extends Actor {
  /**
   * Create the ServiceWorkerRegistrationActor
   * @param DevToolsServerConnection conn
   *   The server connection.
   * @param ServiceWorkerRegistrationInfo registration
   *   The registration's information.
   */
  constructor(conn, registration) {
    super(conn, serviceWorkerRegistrationSpec);
    this._registration = registration;
    this._pushSubscriptionActor = null;

    // A flag to know if preventShutdown has been called and we should
    // try to allow the shutdown of the SW when the actor is destroyed
    this._preventedShutdown = false;

    this._registration.addListener(this);

    this._createServiceWorkerActors();

    Services.obs.addObserver(this, PushService.subscriptionModifiedTopic);
  }

  onChange() {
    this._destroyServiceWorkerActors();
    this._createServiceWorkerActors();
    this.emit("registration-changed");
  }

  form() {
    const registration = this._registration;
    const evaluatingWorker = this._evaluatingWorker.form();
    const installingWorker = this._installingWorker.form();
    const waitingWorker = this._waitingWorker.form();
    const activeWorker = this._activeWorker.form();

    const newestWorker =
      activeWorker || waitingWorker || installingWorker || evaluatingWorker;

    return {
      actor: this.actorID,
      scope: registration.scope,
      url: registration.scriptSpec,
      evaluatingWorker,
      installingWorker,
      waitingWorker,
      activeWorker,
      fetch: newestWorker?.fetch,
      // Check if we have an active worker
      active: !!activeWorker,
      lastUpdateTime: registration.lastUpdateTime,
      traits: {},
    };
  }

  destroy() {
    super.destroy();

    // Ensure resuming the service worker in case the connection drops
    if (this._registration.activeWorker && this._preventedShutdown) {
      this.allowShutdown();
    }

    Services.obs.removeObserver(this, PushService.subscriptionModifiedTopic);
    this._registration.removeListener(this);
    this._registration = null;
    if (this._pushSubscriptionActor) {
      this._pushSubscriptionActor.destroy();
    }
    this._pushSubscriptionActor = null;

    this._destroyServiceWorkerActors();

    this._evaluatingWorker = null;
    this._installingWorker = null;
    this._waitingWorker = null;
    this._activeWorker = null;
  }

  /**
   * Standard observer interface to listen to push messages and changes.
   */
  observe(subject, topic, data) {
    const scope = this._registration.scope;
    if (data !== scope) {
      // This event doesn't concern us, pretend nothing happened.
      return;
    }
    switch (topic) {
      case PushService.subscriptionModifiedTopic:
        if (this._pushSubscriptionActor) {
          this._pushSubscriptionActor.destroy();
          this._pushSubscriptionActor = null;
        }
        this.emit("push-subscription-modified");
        break;
    }
  }

  start() {
    const { activeWorker } = this._registration;

    // TODO: don't return "started" if there's no active worker.
    if (activeWorker) {
      // This starts up the Service Worker if it's not already running.
      // Note that the Service Workers exist in content processes but are
      // managed from the parent process. This is why we call `attachDebugger`
      // here (in the parent process) instead of in a process script.
      activeWorker.attachDebugger();
      activeWorker.detachDebugger();
    }

    return { type: "started" };
  }

  unregister() {
    const { principal, scope } = this._registration;
    const unregisterCallback = {
      unregisterSucceeded() {},
      unregisterFailed() {
        console.error("Failed to unregister the service worker for " + scope);
      },
      QueryInterface: ChromeUtils.generateQI([
        "nsIServiceWorkerUnregisterCallback",
      ]),
    };
    swm.propagateUnregister(principal, unregisterCallback, scope);

    return { type: "unregistered" };
  }

  push() {
    const { principal, scope } = this._registration;
    const originAttributes = ChromeUtils.originAttributesToSuffix(
      principal.originAttributes
    );
    swm.sendPushEvent(originAttributes, scope);
  }

  /**
   * Prevent the current active worker to shutdown after the idle timeout.
   */
  preventShutdown() {
    if (!this._registration.activeWorker) {
      throw new Error(
        "ServiceWorkerRegistrationActor.preventShutdown could not find " +
          "activeWorker in parent-intercept mode"
      );
    }

    // attachDebugger has to be called from the parent process in parent-intercept mode.
    this._registration.activeWorker.attachDebugger();
    this._preventedShutdown = true;
  }

  /**
   * Allow the current active worker to shut down again.
   */
  allowShutdown() {
    if (!this._registration.activeWorker) {
      throw new Error(
        "ServiceWorkerRegistrationActor.allowShutdown could not find " +
          "activeWorker in parent-intercept mode"
      );
    }

    this._registration.activeWorker.detachDebugger();
    this._preventedShutdown = false;
  }

  getPushSubscription() {
    const registration = this._registration;
    let pushSubscriptionActor = this._pushSubscriptionActor;
    if (pushSubscriptionActor) {
      return Promise.resolve(pushSubscriptionActor);
    }
    return new Promise(resolve => {
      PushService.getSubscription(
        registration.scope,
        registration.principal,
        (result, subscription) => {
          if (!subscription) {
            resolve(null);
            return;
          }
          pushSubscriptionActor = new PushSubscriptionActor(
            this.conn,
            subscription
          );
          this._pushSubscriptionActor = pushSubscriptionActor;
          resolve(pushSubscriptionActor);
        }
      );
    });
  }

  _destroyServiceWorkerActors() {
    this._evaluatingWorker.destroy();
    this._installingWorker.destroy();
    this._waitingWorker.destroy();
    this._activeWorker.destroy();
  }

  _createServiceWorkerActors() {
    const { evaluatingWorker, installingWorker, waitingWorker, activeWorker } =
      this._registration;
    const origin = this._registration.principal.origin;

    this._evaluatingWorker = new ServiceWorkerActor(
      this.conn,
      evaluatingWorker,
      origin
    );
    this._installingWorker = new ServiceWorkerActor(
      this.conn,
      installingWorker,
      origin
    );
    this._waitingWorker = new ServiceWorkerActor(
      this.conn,
      waitingWorker,
      origin
    );
    this._activeWorker = new ServiceWorkerActor(
      this.conn,
      activeWorker,
      origin
    );

    // Add the ServiceWorker actors as children of this ServiceWorkerRegistration actor,
    // assigning them valid actorIDs.
    this.manage(this._evaluatingWorker);
    this.manage(this._installingWorker);
    this.manage(this._waitingWorker);
    this.manage(this._activeWorker);
  }
}

exports.ServiceWorkerRegistrationActor = ServiceWorkerRegistrationActor;
