/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* 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 { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;

Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm');

const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
                      .getService(Ci.nsIUUIDGenerator);

function debug(str) {
  // dump('DEBUG -*- PresentationSessionChromeScript1UA -*-: ' + str + '\n');
}

const originalFactoryData = [];
var sessionId; // Store the uuid generated by PresentationRequest.
var triggerControlChannelError = false; // For simulating error during control channel establishment.

// control channel of sender
const mockControlChannelOfSender = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
  set listener(listener) {
    // PresentationControllingInfo::SetControlChannel
    if (listener) {
      debug('set listener for mockControlChannelOfSender without null');
    } else {
      debug('set listener for mockControlChannelOfSender with null');
    }
    this._listener = listener;
  },
  get listener() {
    return this._listener;
  },
  notifyConnected: function() {
    // send offer after notifyConnected immediately
    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .notifyConnected();
  },
  notifyReconnected: function() {
    // send offer after notifyOpened immediately
    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .notifyReconnected();
  },
  sendOffer: function(offer) {
    Services.tm.mainThread.dispatch(() => {
      mockControlChannelOfReceiver.onOffer(offer);
    }, Ci.nsIThread.DISPATCH_NORMAL);
  },
  onAnswer: function(answer) {
    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .onAnswer(answer);
  },
  launch: function(presentationId, url) {
    sessionId = presentationId;
    sendAsyncMessage('sender-launch', url);
  },
  disconnect: function(reason) {
    if (!this._listener) {
      return;
    }
    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .notifyDisconnected(reason);
    mockControlChannelOfReceiver.disconnect();
  },
  terminate: function(presentationId) {
    sendAsyncMessage('sender-terminate');
  },
  reconnect: function(presentationId, url) {
    sendAsyncMessage('start-reconnect', url);
  },
  sendIceCandidate: function(candidate) {
    mockControlChannelOfReceiver.notifyIceCandidate(candidate);
  },
  notifyIceCandidate: function(candidate) {
    if (!this._listener) {
      return;
    }

    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .onIceCandidate(candidate);
  },
};

// control channel of receiver
const mockControlChannelOfReceiver = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
  set listener(listener) {
    // PresentationPresentingInfo::SetControlChannel
    if (listener) {
      debug('set listener for mockControlChannelOfReceiver without null');
    } else {
      debug('set listener for mockControlChannelOfReceiver with null');
    }
    this._listener = listener;

    if (this._pendingOpened) {
      this._pendingOpened = false;
      this.notifyConnected();
    }
  },
  get listener() {
    return this._listener;
  },
  notifyConnected: function() {
    // do nothing
    if (!this._listener) {
      this._pendingOpened = true;
      return;
    }
    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .notifyConnected();
  },
  onOffer: function(offer) {
    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .onOffer(offer);
  },
  sendAnswer: function(answer) {
    Services.tm.mainThread.dispatch(() => {
      mockControlChannelOfSender.onAnswer(answer);
    }, Ci.nsIThread.DISPATCH_NORMAL);
  },
  disconnect: function(reason) {
    if (!this._listener) {
      return;
    }

    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .notifyDisconnected(reason);
    sendAsyncMessage('control-channel-receiver-closed', reason);
  },
  terminate: function(presentaionId) {
  },
  sendIceCandidate: function(candidate) {
    mockControlChannelOfReceiver.notifyIceCandidate(candidate);
  },
  notifyIceCandidate: function(candidate) {
    if (!this._listener) {
      return;
    }

    this._listener
        .QueryInterface(Ci.nsIPresentationControlChannelListener)
        .onIceCandidate(candidate);
  },
};

const mockDevice = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
  id:   'id',
  name: 'name',
  type: 'type',
  establishControlChannel: function(url, presentationId) {
    if (triggerControlChannelError) {
      throw Cr.NS_ERROR_FAILURE;
    }
    sendAsyncMessage('control-channel-established');
    return mockControlChannelOfSender;
  },
  disconnect: function() {
    sendAsyncMessage('device-disconnected');
  },
  isRequestedUrlSupported: function(requestedUrl) {
    return true;
  },
};

const mockDevicePrompt = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt,
                                         Ci.nsIFactory]),
  createInstance: function(aOuter, aIID) {
    if (aOuter) {
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    }
    return this.QueryInterface(aIID);
  },
  set request(request) {
    this._request = request;
  },
  get request() {
    return this._request;
  },
  promptDeviceSelection: function(request) {
    this._request = request;
    sendAsyncMessage('device-prompt');
  },
  simulateSelect: function() {
    this._request.select(mockDevice);
  },
  simulateCancel: function() {
    this._request.cancel();
  }
};

const mockRequestUIGlue = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue,
                                         Ci.nsIFactory]),
  set promise(aPromise) {
    this._promise = aPromise
  },
  get promise() {
    return this._promise;
  },
  createInstance: function(aOuter, aIID) {
    if (aOuter) {
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    }
    return this.QueryInterface(aIID);
  },
  sendRequest: function(aUrl, aSessionId) {
    return this.promise;
  },
};

function initMockAndListener() {

  function registerMockFactory(contractId, mockClassId, mockFactory) {
    var originalClassId, originalFactory;

    var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
    if (!registrar.isCIDRegistered(mockClassId)) {
      try {
        originalClassId = registrar.contractIDToCID(contractId);
        originalFactory = Cm.getClassObject(Cc[contractId], Ci.nsIFactory);
      } catch (ex) {
        originalClassId = "";
        originalFactory = null;
      }
      if (originalFactory) {
        registrar.unregisterFactory(originalClassId, originalFactory);
      }
      registrar.registerFactory(mockClassId, "", contractId, mockFactory);
    }

    return { contractId: contractId,
             mockClassId: mockClassId,
             mockFactory: mockFactory,
             originalClassId: originalClassId,
             originalFactory: originalFactory };
  }
  // Register mock factories.
  const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
                        .getService(Ci.nsIUUIDGenerator);
  originalFactoryData.push(registerMockFactory("@mozilla.org/presentation-device/prompt;1",
                                               uuidGenerator.generateUUID(),
                                               mockDevicePrompt));
  originalFactoryData.push(registerMockFactory("@mozilla.org/presentation/requestuiglue;1",
                                               uuidGenerator.generateUUID(),
                                               mockRequestUIGlue));

  addMessageListener('trigger-device-add', function() {
    debug('Got message: trigger-device-add');
    var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
                        .getService(Ci.nsIPresentationDeviceManager);
    deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
                 .addDevice(mockDevice);
  });

  addMessageListener('trigger-device-prompt-select', function() {
    debug('Got message: trigger-device-prompt-select');
    mockDevicePrompt.simulateSelect();
  });

  addMessageListener('trigger-on-session-request', function(url) {
    debug('Got message: trigger-on-session-request');
    var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
                          .getService(Ci.nsIPresentationDeviceManager);
    deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
                 .onSessionRequest(mockDevice,
                                   url,
                                   sessionId,
                                   mockControlChannelOfReceiver);
  });

  addMessageListener('trigger-on-terminate-request', function() {
    debug('Got message: trigger-on-terminate-request');
    var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
                          .getService(Ci.nsIPresentationDeviceManager);
    deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
                 .onTerminateRequest(mockDevice,
                                     sessionId,
                                     mockControlChannelOfReceiver,
                                     false);
  });

  addMessageListener('trigger-control-channel-open', function(reason) {
    debug('Got message: trigger-control-channel-open');
    mockControlChannelOfSender.notifyConnected();
    mockControlChannelOfReceiver.notifyConnected();
  });

  addMessageListener('trigger-control-channel-error', function(reason) {
    debug('Got message: trigger-control-channel-open');
    triggerControlChannelError = true;
  });

  addMessageListener('trigger-reconnected-acked', function(url) {
    debug('Got message: trigger-reconnected-acked');
    mockControlChannelOfSender.notifyReconnected();
    var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
                          .getService(Ci.nsIPresentationDeviceManager);
    deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
                 .onReconnectRequest(mockDevice,
                                     url,
                                     sessionId,
                                     mockControlChannelOfReceiver);
  });

  // Used to call sendAsyncMessage in chrome script from receiver.
  addMessageListener('forward-command', function(command_data) {
    let command = JSON.parse(command_data);
    sendAsyncMessage(command.name, command.data);
  });

  addMessageListener('teardown', teardown);

  var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
  obs.addObserver(function setupRequestPromiseHandler(aSubject, aTopic, aData) {
    debug('Got observer: setup-request-promise');
    obs.removeObserver(setupRequestPromiseHandler, aTopic);
    mockRequestUIGlue.promise = aSubject;
    sendAsyncMessage('promise-setup-ready');
  }, 'setup-request-promise', false);
}

function teardown() {

  function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) {
    if (originalFactory) {
      var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
      registrar.unregisterFactory(mockedClassId, mockedFactory);
      registrar.registerFactory(originalClassId, "", contractId, originalFactory);
    }
  }

  mockRequestUIGlue.promise               = null;
  mockControlChannelOfSender.listener     = null;
  mockControlChannelOfReceiver.listener   = null;
  mockDevicePrompt.request                = null;

  var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
                      .getService(Ci.nsIPresentationDeviceManager);
  deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
               .removeDevice(mockDevice);
  // Register original factories.
  for (var data of originalFactoryData) {
    registerOriginalFactory(data.contractId, data.mockClassId,
                            data.mockFactory, data.originalClassId,
                            data.originalFactory);
  }
  sendAsyncMessage('teardown-complete');
}

initMockAndListener();
