/* Copyright 2012 Mozilla Foundation and Mozilla contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* Copyright © 2013, Deutsche Telekom, Inc. */

"use strict";

const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

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

XPCOMUtils.defineLazyGetter(this, "NFC", function () {
  let obj = {};
  Cu.import("resource://gre/modules/nfc_consts.js", obj);
  return obj;
});

Cu.import("resource://gre/modules/systemlibs.js");
const NFC_ENABLED = libcutils.property_get("ro.moz.nfc.enabled", "false") === "true";

// set to true to in nfc_consts.js to see debug messages
var DEBUG = NFC.DEBUG_CONTENT_HELPER;

var debug;
function updateDebug() {
  if (DEBUG || NFC.DEBUG_CONTENT_HELPER) {
    debug = function (s) {
      dump("-*- NfcContentHelper: " + s + "\n");
    };
  } else {
    debug = function (s) {};
  }
};
updateDebug();

const NFCCONTENTHELPER_CID =
  Components.ID("{4d72c120-da5f-11e1-9b23-0800200c9a66}");

const NFC_IPC_MSG_NAMES = [
  "NFC:ReadNDEFResponse",
  "NFC:WriteNDEFResponse",
  "NFC:MakeReadOnlyResponse",
  "NFC:FormatResponse",
  "NFC:TransceiveResponse",
  "NFC:CheckP2PRegistrationResponse",
  "NFC:DOMEvent",
  "NFC:NotifySendFileStatusResponse",
  "NFC:ChangeRFStateResponse"
];

XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                   "@mozilla.org/childprocessmessagemanager;1",
                                   "nsISyncMessageSender");

function NfcContentHelper() {
  Services.obs.addObserver(this, "xpcom-shutdown", false);

  this._requestMap = [];
  this.initDOMRequestHelper(/* window */ null, NFC_IPC_MSG_NAMES);
  this.eventListeners = {};
}

NfcContentHelper.prototype = {
  __proto__: DOMRequestIpcHelper.prototype,

  QueryInterface: XPCOMUtils.generateQI([Ci.nsINfcContentHelper,
                                         Ci.nsINfcBrowserAPI,
                                         Ci.nsISupportsWeakReference,
                                         Ci.nsIObserver]),
  classID:   NFCCONTENTHELPER_CID,
  classInfo: XPCOMUtils.generateCI({
    classID:          NFCCONTENTHELPER_CID,
    classDescription: "NfcContentHelper",
    interfaces:       [Ci.nsINfcContentHelper,
                       Ci.nsINfcBrowserAPI]
  }),

  _requestMap: null,
  eventListeners: null,

  queryRFState: function queryRFState() {
    return cpmm.sendSyncMessage("NFC:QueryInfo")[0].rfState;
  },

  setFocusTab: function setFocusTab(tabId, isFocus) {
    cpmm.sendAsyncMessage("NFC:SetFocusTab", {
      tabId: tabId,
      isFocus: isFocus
    });
  },

  // NFCTag interface
  readNDEF: function readNDEF(sessionToken, callback) {
    let requestId = callback.getCallbackId();
    this._requestMap[requestId] = callback;

    cpmm.sendAsyncMessage("NFC:ReadNDEF", {
      requestId: requestId,
      sessionToken: sessionToken
    });
  },

  writeNDEF: function writeNDEF(records, isP2P, sessionToken, callback) {
    let requestId = callback.getCallbackId();
    this._requestMap[requestId] = callback;

    cpmm.sendAsyncMessage("NFC:WriteNDEF", {
      requestId: requestId,
      sessionToken: sessionToken,
      records: records,
      isP2P: isP2P
    });
  },

  makeReadOnly: function makeReadOnly(sessionToken, callback) {
    let requestId = callback.getCallbackId();
    this._requestMap[requestId] = callback;

    cpmm.sendAsyncMessage("NFC:MakeReadOnly", {
      requestId: requestId,
      sessionToken: sessionToken
    });
  },

  format: function format(sessionToken, callback) {
    let requestId = callback.getCallbackId();
    this._requestMap[requestId] = callback;

    cpmm.sendAsyncMessage("NFC:Format", {
      requestId: requestId,
      sessionToken: sessionToken
    });
  },

  transceive: function transceive(sessionToken, technology, command, callback) {
    let requestId = callback.getCallbackId();
    this._requestMap[requestId] = callback;

    cpmm.sendAsyncMessage("NFC:Transceive", {
      requestId: requestId,
      sessionToken: sessionToken,
      technology: technology,
      command: command
    });
  },

  sendFile: function sendFile(data, sessionToken, callback) {
    let requestId = callback.getCallbackId();
    this._requestMap[requestId] = callback;

    cpmm.sendAsyncMessage("NFC:SendFile", {
      requestId: requestId,
      sessionToken: sessionToken,
      blob: data.blob
    });
  },

  notifySendFileStatus: function notifySendFileStatus(status, requestId) {
    cpmm.sendAsyncMessage("NFC:NotifySendFileStatus", {
      status: status,
      requestId: requestId
    });
  },

  addEventListener: function addEventListener(listener, tabId) {
    let _window = listener.window;

    this.eventListeners[tabId] = listener;
    cpmm.sendAsyncMessage("NFC:AddEventListener", { tabId: tabId });
  },

  removeEventListener: function removeEventListener(tabId) {
    delete this.eventListeners[tabId];

    cpmm.sendAsyncMessage("NFC:RemoveEventListener", { tabId: tabId });
  },

  registerTargetForPeerReady: function registerTargetForPeerReady(appId) {
    cpmm.sendAsyncMessage("NFC:RegisterPeerReadyTarget", { appId: appId });
  },

  unregisterTargetForPeerReady: function unregisterTargetForPeerReady(appId) {
    cpmm.sendAsyncMessage("NFC:UnregisterPeerReadyTarget", { appId: appId });
  },

  checkP2PRegistration: function checkP2PRegistration(appId, callback) {
    let requestId = callback.getCallbackId();
    this._requestMap[requestId] = callback;

    cpmm.sendAsyncMessage("NFC:CheckP2PRegistration", {
      appId: appId,
      requestId: requestId
    });
  },

  notifyUserAcceptedP2P: function notifyUserAcceptedP2P(appId) {
    cpmm.sendAsyncMessage("NFC:NotifyUserAcceptedP2P", {
      appId: appId
    });
  },

  changeRFState: function changeRFState(rfState, callback) {
    let requestId = callback.getCallbackId();
    this._requestMap[requestId] = callback;

    cpmm.sendAsyncMessage("NFC:ChangeRFState",
                          {requestId: requestId,
                           rfState: rfState});
  },

  callDefaultFoundHandler: function callDefaultFoundHandler(sessionToken,
                                                            isP2P,
                                                            records) {
    cpmm.sendAsyncMessage("NFC:CallDefaultFoundHandler",
                          {sessionToken: sessionToken,
                           isP2P: isP2P,
                           records: records});
  },

  callDefaultLostHandler: function callDefaultLostHandler(sessionToken, isP2P) {
    cpmm.sendAsyncMessage("NFC:CallDefaultLostHandler",
                          {sessionToken: sessionToken,
                           isP2P: isP2P});
  },

  // nsIObserver
  observe: function observe(subject, topic, data) {
    if (topic == "xpcom-shutdown") {
      this.destroyDOMRequestHelper();
      Services.obs.removeObserver(this, "xpcom-shutdown");
      cpmm = null;
    }
  },

  // nsIMessageListener
  receiveMessage: function receiveMessage(message) {
    DEBUG && debug("Message received: " + JSON.stringify(message));
    let result = message.data;

    switch (message.name) {
      case "NFC:ReadNDEFResponse":
        this.handleReadNDEFResponse(result);
        break;
      case "NFC:CheckP2PRegistrationResponse":
        this.handleCheckP2PRegistrationResponse(result);
        break;
      case "NFC:TransceiveResponse":
        this.handleTransceiveResponse(result);
        break;
      case "NFC:WriteNDEFResponse": // Fall through.
      case "NFC:MakeReadOnlyResponse":
      case "NFC:FormatResponse":
      case "NFC:NotifySendFileStatusResponse":
      case "NFC:ChangeRFStateResponse":
        this.handleGeneralResponse(result);
        break;
      case "NFC:DOMEvent":
        this.handleDOMEvent(result);
        break;
    }
  },

  handleGeneralResponse: function handleGeneralResponse(result) {
    let requestId = result.requestId;
    let callback = this._requestMap[requestId];
    if (!callback) {
      debug("not firing message " + result.type + " for id: " + requestId);
      return;
    }
    delete this._requestMap[requestId];

    if (result.errorMsg) {
      callback.notifyError(result.errorMsg);
    } else {
      callback.notifySuccess();
    }
  },

  handleReadNDEFResponse: function handleReadNDEFResponse(result) {
    let requestId = result.requestId;
    let callback = this._requestMap[requestId];
    if (!callback) {
      debug("not firing message handleReadNDEFResponse for id: " + requestId);
      return;
    }
    delete this._requestMap[requestId];

    if (result.errorMsg) {
      callback.notifyError(result.errorMsg);
      return;
    }

    callback.notifySuccessWithNDEFRecords(result.records);
  },

  handleCheckP2PRegistrationResponse: function handleCheckP2PRegistrationResponse(result) {
    let requestId = result.requestId;
    let callback = this._requestMap[requestId];
    if (!callback) {
      debug("not firing message handleCheckP2PRegistrationResponse for id: " + requestId);
      return;
    }
    delete this._requestMap[requestId];

    // Privilaged status API. Always fire success to avoid using exposed props.
    // The receiver must check the boolean mapped status code to handle.
    callback.notifySuccessWithBoolean(!result.errorMsg);
  },

  handleTransceiveResponse: function handleTransceiveResponse(result) {
    let requestId = result.requestId;
    let callback = this._requestMap[requestId];
    if (!callback) {
      debug("not firing message handleTransceiveResponse for id: " + requestId);
      return;
    }
    delete this._requestMap[requestId];

    if (result.errorMsg) {
      callback.notifyError(result.errorMsg);
      return;
    }

    callback.notifySuccessWithByteArray(result.response);
  },

  handleDOMEvent: function handleDOMEvent(result) {
    let listener = this.eventListeners[result.tabId];
    if (!listener) {
      debug("no listener for tabId " + result.tabId);
      return;
    }

    switch (result.event) {
      case NFC.PEER_EVENT_READY:
        listener.notifyPeerFound(result.sessionToken, /* isPeerReady */ true);
        break;
      case NFC.PEER_EVENT_FOUND:
        listener.notifyPeerFound(result.sessionToken);
        break;
      case NFC.PEER_EVENT_LOST:
        listener.notifyPeerLost(result.sessionToken);
        break;
      case NFC.TAG_EVENT_FOUND:
        let ndefInfo = null;
        if (result.tagType !== undefined &&
            result.maxNDEFSize !== undefined &&
            result.isReadOnly !== undefined &&
            result.isFormatable !== undefined) {
          ndefInfo = new TagNDEFInfo(result.tagType,
                                     result.maxNDEFSize,
                                     result.isReadOnly,
                                     result.isFormatable);
        }

        let tagInfo = new TagInfo(result.techList, result.tagId);
        listener.notifyTagFound(result.sessionToken,
                                          tagInfo,
                                          ndefInfo,
                                          result.records);
        break;
      case NFC.TAG_EVENT_LOST:
        listener.notifyTagLost(result.sessionToken);
        break;
      case NFC.RF_EVENT_STATE_CHANGED:
        listener.notifyRFStateChanged(result.rfState);
        break;
      case NFC.FOCUS_CHANGED:
        listener.notifyFocusChanged(result.focus);
        break;
    }
  }
};

function TagNDEFInfo(tagType, maxNDEFSize, isReadOnly, isFormatable) {
  this.tagType = tagType;
  this.maxNDEFSize = maxNDEFSize;
  this.isReadOnly = isReadOnly;
  this.isFormatable = isFormatable;
}
TagNDEFInfo.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsITagNDEFInfo]),

  tagType: null,
  maxNDEFSize: 0,
  isReadOnly: false,
  isFormatable: false
};

function TagInfo(techList, tagId) {
  this.techList = techList;
  this.tagId = tagId;
}
TagInfo.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsITagInfo]),

  techList: null,
  tagId: null,
};

if (NFC_ENABLED) {
  this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NfcContentHelper]);
}
