/* 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, results: Cr, utils: Cu } = Components;

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

let { PlatformInfo } = ExtensionUtils;

if (PlatformInfo.os == "android" && !Services.prefs.getBoolPref("network.mdns.use_js_fallback")) {
  Cu.import("resource://gre/modules/MulticastDNSAndroid.jsm");
} else {
  Cu.import("resource://gre/modules/MulticastDNS.jsm");
}

const DNSSERVICEDISCOVERY_CID = Components.ID("{f9346d98-f27a-4e89-b744-493843416480}");
const DNSSERVICEDISCOVERY_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-sd;1";
const DNSSERVICEINFO_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-info;1";

function log(aMsg) {
  dump("-*- nsDNSServiceDiscovery.js : " + aMsg + "\n");
}

function generateUuid() {
  var uuidGenerator = Components.classes["@mozilla.org/uuid-generator;1"].
    getService(Ci.nsIUUIDGenerator);
  return uuidGenerator.generateUUID().toString();
}

// Helper class to transform return objects to correct type.
function ListenerWrapper(aListener, aMdns) {
  this.listener = aListener;
  this.mdns = aMdns;

  this.discoveryStarting = false;
  this.stopDiscovery = false;

  this.registrationStarting = false;
  this.stopRegistration = false;

  this.uuid = generateUuid();
}

ListenerWrapper.prototype = {
  // Helper function for transforming an Object into nsIDNSServiceInfo.
  makeServiceInfo: function (aServiceInfo) {
    let serviceInfo = Cc[DNSSERVICEINFO_CONTRACT_ID].createInstance(Ci.nsIDNSServiceInfo);

    for (let name of ['host', 'address', 'port', 'serviceName', 'serviceType']) {
      try {
        serviceInfo[name] = aServiceInfo[name];
      } catch (e) {
        // ignore exceptions
      }
    }

    let attributes;
    try {
      attributes = _toPropertyBag2(aServiceInfo.attributes);
    } catch (err) {
        // Ignore unset attributes in object.
        log("Caught unset attributes error: " + err + " - " + err.stack);
        attributes = Cc['@mozilla.org/hash-property-bag;1']
                        .createInstance(Ci.nsIWritablePropertyBag2);
    }
    serviceInfo.attributes = attributes;

    return serviceInfo;
  },

  /* transparent types */
  onDiscoveryStarted: function(aServiceType) {
    this.discoveryStarting = false;
    this.listener.onDiscoveryStarted(aServiceType);

    if (this.stopDiscovery) {
      this.mdns.stopDiscovery(aServiceType, this);
    }
  },
  onDiscoveryStopped: function(aServiceType) {
    this.listener.onDiscoveryStopped(aServiceType);
  },
  onStartDiscoveryFailed: function(aServiceType, aErrorCode) {
    log('onStartDiscoveryFailed: ' + aServiceType + ' (' + aErrorCode + ')');
    this.discoveryStarting = false;
    this.stopDiscovery = true;
    this.listener.onStartDiscoveryFailed(aServiceType, aErrorCode);
  },
  onStopDiscoveryFailed: function(aServiceType, aErrorCode) {
    log('onStopDiscoveryFailed: ' + aServiceType + ' (' + aErrorCode + ')');
    this.listener.onStopDiscoveryFailed(aServiceType, aErrorCode);
  },

  /* transform types */
  onServiceFound: function(aServiceInfo) {
    this.listener.onServiceFound(this.makeServiceInfo(aServiceInfo));
  },
  onServiceLost: function(aServiceInfo) {
    this.listener.onServiceLost(this.makeServiceInfo(aServiceInfo));
  },
  onServiceRegistered: function(aServiceInfo) {
    this.registrationStarting = false;
    this.listener.onServiceRegistered(this.makeServiceInfo(aServiceInfo));

    if (this.stopRegistration) {
      this.mdns.unregisterService(aServiceInfo, this);
    }
  },
  onServiceUnregistered: function(aServiceInfo) {
    this.listener.onServiceUnregistered(this.makeServiceInfo(aServiceInfo));
  },
  onServiceResolved: function(aServiceInfo) {
    this.listener.onServiceResolved(this.makeServiceInfo(aServiceInfo));
  },

  onRegistrationFailed: function(aServiceInfo, aErrorCode) {
    log('onRegistrationFailed: (' + aErrorCode + ')');
    this.registrationStarting = false;
    this.stopRegistration = true;
    this.listener.onRegistrationFailed(this.makeServiceInfo(aServiceInfo), aErrorCode);
  },
  onUnregistrationFailed: function(aServiceInfo, aErrorCode) {
    log('onUnregistrationFailed: (' + aErrorCode + ')');
    this.listener.onUnregistrationFailed(this.makeServiceInfo(aServiceInfo), aErrorCode);
  },
  onResolveFailed: function(aServiceInfo, aErrorCode) {
    log('onResolveFailed: (' + aErrorCode + ')');
    this.listener.onResolveFailed(this.makeServiceInfo(aServiceInfo), aErrorCode);
  }
};

function nsDNSServiceDiscovery() {
  log("nsDNSServiceDiscovery");
  this.mdns = new MulticastDNS();
}

nsDNSServiceDiscovery.prototype = {
  classID: DNSSERVICEDISCOVERY_CID,
  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIDNSServiceDiscovery]),

  startDiscovery: function(aServiceType, aListener) {
    log("startDiscovery");
    let listener = new ListenerWrapper(aListener, this.mdns);
    listener.discoveryStarting = true;
    this.mdns.startDiscovery(aServiceType, listener);

    return {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
      cancel: (function() {
        if (this.discoveryStarting || this.stopDiscovery) {
          this.stopDiscovery = true;
          return;
        }
        this.mdns.stopDiscovery(aServiceType, listener);
      }).bind(listener)
    };
  },

  registerService: function(aServiceInfo, aListener) {
    log("registerService");
    let listener = new ListenerWrapper(aListener, this.mdns);
    listener.registrationStarting = true;
    this.mdns.registerService(aServiceInfo, listener);

    return {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
      cancel: (function() {
        if (this.registrationStarting || this.stopRegistration) {
          this.stopRegistration = true;
          return;
        }
        this.mdns.unregisterService(aServiceInfo, listener);
      }).bind(listener)
    };
  },

  resolveService: function(aServiceInfo, aListener) {
    log("resolveService");
    this.mdns.resolveService(aServiceInfo, new ListenerWrapper(aListener));
  }
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsDNSServiceDiscovery]);

function _toPropertyBag2(obj)
{
  if (obj.QueryInterface) {
    return obj.QueryInterface(Ci.nsIPropertyBag2);
  }

  let result = Cc['@mozilla.org/hash-property-bag;1']
                  .createInstance(Ci.nsIWritablePropertyBag2);
  for (let name in obj) {
    result.setPropertyAsAString(name, obj[name]);
  }
  return result;
}
