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

const {protocol} = ChromeUtils.importESModule("chrome://juggler/content/protocol/Protocol.js");
const {checkScheme} = ChromeUtils.importESModule("chrome://juggler/content/protocol/PrimitiveTypes.js");
const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');

const helper = new Helper();

export class Dispatcher {
  /**
   * @param {Connection} connection
   */
  constructor(connection) {
    this._connection = connection;
    this._connection.onmessage = this._dispatch.bind(this);
    this._connection.onclose = this._dispose.bind(this);
    this._sessions = new Map();
    this._rootSession = new ProtocolSession(this, undefined);
  }

  rootSession() {
    return this._rootSession;
  }

  createSession() {
    const session = new ProtocolSession(this, helper.generateId());
    this._sessions.set(session.sessionId(), session);
    return session;
  }

  destroySession(session) {
    this._sessions.delete(session.sessionId());
    session._dispose();
  }

  _dispose() {
    this._connection.onmessage = null;
    this._connection.onclose = null;
    this._rootSession._dispose();
    this._rootSession = null;
    this._sessions.clear();
  }

  async _dispatch(event) {
    const data = JSON.parse(event.data);
    const id = data.id;
    const sessionId = data.sessionId;
    delete data.sessionId;
    try {
      const session = sessionId ? this._sessions.get(sessionId) : this._rootSession;
      if (!session)
        throw new Error(`ERROR: cannot find session with id "${sessionId}"`);
      const method = data.method;
      const params = data.params || {};
      if (!id)
        throw new Error(`ERROR: every message must have an 'id' parameter`);
      if (!method)
        throw new Error(`ERROR: every message must have a 'method' parameter`);

      const [domain, methodName] = method.split('.');
      const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null;
      if (!descriptor)
        throw new Error(`ERROR: method '${method}' is not supported`);
      let details = {};
      if (!checkScheme(descriptor.params || {}, params, details))
        throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`);

      const result = await session.dispatch(method, params);

      details = {};
      if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details))
        throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`);

      this._connection.send(JSON.stringify({id, sessionId, result}));
    } catch (e) {
      dump(`
        ERROR: ${e.message} ${e.stack}
      `);
      this._connection.send(JSON.stringify({id, sessionId, error: {
        message: e.message,
        data: e.stack
      }}));
    }
  }

  _emitEvent(sessionId, eventName, params) {
    const [domain, eName] = eventName.split('.');
    const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null;
    if (!scheme)
      throw new Error(`ERROR: event '${eventName}' is not supported`);
    const details = {};
    if (!checkScheme(scheme, params || {}, details))
      throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`);
    this._connection.send(JSON.stringify({method: eventName, params, sessionId}));
  }
}

class ProtocolSession {
  constructor(dispatcher, sessionId) {
    this._sessionId = sessionId;
    this._dispatcher = dispatcher;
    this._handler = null;
  }

  sessionId() {
    return this._sessionId;
  }

  setHandler(handler) {
    this._handler = handler;
  }

  _dispose() {
    if (this._handler)
      this._handler.dispose();
    this._handler = null;
    this._dispatcher = null;
  }

  emitEvent(eventName, params) {
    if (!this._dispatcher)
      throw new Error(`Session has been disposed.`);
    this._dispatcher._emitEvent(this._sessionId, eventName, params);
  }

  async dispatch(method, params) {
    if (!this._handler)
      throw new Error(`Session does not have a handler!`);
    if (!this._handler[method])
      throw new Error(`Handler for does not implement method "${method}"`);
    return await this._handler[method](params);
  }
}
