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

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

"use strict";

/* globals dump, Components, XPCOMUtils, DOMRequestIpcHelper, cpmm, SE  */

const DEBUG = false;
function debug(s) {
  if (DEBUG) {
    dump("-*- SecureElement DOM: " + s + "\n");
  }
}

const Ci = Components.interfaces;
const Cu = Components.utils;

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

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

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

function PromiseHelpersSubclass(win) {
  this._window = win;
}

PromiseHelpersSubclass.prototype = {
  __proto__: DOMRequestIpcHelper.prototype,

  _window: null,

  _context: [],

  createSEPromise: function createSEPromise(callback, /* optional */ ctx) {
    let ctxCallback = (resolverId) => {
      if (ctx) {
        this._context[resolverId] = ctx;
      }

      callback(resolverId);
    };

    return this.createPromise((resolve, reject) => {
      let resolverId = this.getPromiseResolverId({
        resolve: resolve,
        reject: reject
      });
      ctxCallback(resolverId);
    });
  },

  takePromise: function takePromise(resolverId) {
    let resolver = this.takePromiseResolver(resolverId);
    if (!resolver) {
      return;
    }

    // Get the context associated with this resolverId
    let context = this._context[resolverId];
    delete this._context[resolverId];

    return {resolver: resolver, context: context};
  },

  rejectWithSEError: function rejectWithSEError(reason) {
    return this.createSEPromise((resolverId) => {
      debug("rejectWithSEError : " + reason);
      this.takePromiseResolver(resolverId).reject(new Error(reason));
    });
  }
};

// Helper wrapper class to do promises related chores
let PromiseHelpers;

/**
 * Instance of 'SEReaderImpl' class is the connector to a secure element.
 * A reader may or may not have a secure element present, since some
 * secure elements are removable in nature (eg:- 'uicc'). These
 * Readers can be physical devices or virtual devices.
 */
function SEReaderImpl() {}

SEReaderImpl.prototype = {
  _window: null,

  _sessions: [],

  type: null,

  classID: Components.ID("{1c7bdba3-cd35-4f8b-a546-55b3232457d5}"),
  contractID: "@mozilla.org/secureelement/reader;1",
  QueryInterface: XPCOMUtils.generateQI([]),

  // Chrome-only function
  onSessionClose: function onSessionClose(sessionCtx) {
    let index = this._sessions.indexOf(sessionCtx);
    if (index != -1) {
      this._sessions.splice(index, 1);
    }
  },

  initialize: function initialize(win, type) {
    this._window = win;
    this.type = type;
  },

  openSession: function openSession() {
    return PromiseHelpers.createSEPromise((resolverId) => {
      let chromeObj = new SESessionImpl();
      chromeObj.initialize(this._window, this);
      let contentObj = this._window.SESession._create(this._window, chromeObj);
      this._sessions.push(contentObj);
      PromiseHelpers.takePromiseResolver(resolverId).resolve(contentObj);
    });
  },

  closeAll: function closeAll() {
    return PromiseHelpers.createSEPromise((resolverId) => {
      let promises = [];
      for (let session of this._sessions) {
        if (!session.isClosed) {
          promises.push(session.closeAll());
        }
      }

      let resolver = PromiseHelpers.takePromiseResolver(resolverId);
      // Wait till all the promises are resolved
      Promise.all(promises).then(() => {
        this._sessions = [];
        resolver.resolve();
      }, (reason) => {
        resolver.reject(new Error(SE.ERROR_BADSTATE +
          " Unable to close all channels associated with this reader"));
      });
    });
  },

  get isSEPresent() {
    // TODO: Bug 1119152 - Implement new idl with interfaces to detect
    //                     secureelement state changes.
    return true;
  }
};

/**
 * Instance of 'SESessionImpl' object represent a connection session
 * to one of the secure elements available on the device.
 * These objects can be used to get a communication channel with an application
 * hosted by the Secure Element.
 */
function SESessionImpl() {}

SESessionImpl.prototype = {
  _window: null,

  _channels: [],

  _isClosed: false,

  _reader: null,

  classID: Components.ID("{2b1809f8-17bd-4947-abd7-bdef1498561c}"),
  contractID: "@mozilla.org/secureelement/session;1",
  QueryInterface: XPCOMUtils.generateQI([]),

  // Private function
  _checkClosed: function _checkClosed() {
    if (this._isClosed) {
      throw new Error(SE.ERROR_BADSTATE + " Session Already Closed!");
    }
  },

  // Chrome-only function
  onChannelOpen: function onChannelOpen(channelCtx) {
    this._channels.push(channelCtx);
  },

  // Chrome-only function
  onChannelClose: function onChannelClose(channelCtx) {
    let index = this._channels.indexOf(channelCtx);
    if (index != -1) {
      this._channels.splice(index, 1);
    }
  },

  initialize: function initialize(win, readerCtx) {
    this._window = win;
    this._reader = readerCtx;
  },

  openLogicalChannel: function openLogicalChannel(aid) {
    this._checkClosed();

    let aidLen = aid ? aid.length : 0;
    if (aidLen < SE.MIN_AID_LEN || aidLen > SE.MAX_AID_LEN) {
      return PromiseHelpers.rejectWithSEError(SE.ERROR_GENERIC +
             " Invalid AID length - " + aidLen);
    }

    return PromiseHelpers.createSEPromise((resolverId) => {
      /**
       * @params for 'SE:OpenChannel'
       *
       * resolverId  : ID that identifies this IPC request.
       * aid         : AID that identifies the applet on SecureElement
       * type        : Reader type ('uicc' / 'eSE')
       * appId       : Current appId obtained from 'Principal' obj
       */
      cpmm.sendAsyncMessage("SE:OpenChannel", {
        resolverId: resolverId,
        aid: aid,
        type: this.reader.type,
        appId: this._window.document.nodePrincipal.appId
      });
    }, this);
  },

  closeAll: function closeAll() {
    this._checkClosed();

    return PromiseHelpers.createSEPromise((resolverId) => {
      let promises = [];
      for (let channel of this._channels) {
        if (!channel.isClosed) {
          promises.push(channel.close());
        }
      }

      let resolver = PromiseHelpers.takePromiseResolver(resolverId);
      Promise.all(promises).then(() => {
        this._isClosed = true;
        this._channels = [];
        // Notify parent of this session instance's closure, so that its
        // instance entry can be removed from the parent as well.
        this._reader.onSessionClose(this.__DOM_IMPL__);
        resolver.resolve();
      }, (reason) => {
        resolver.reject(new Error(SE.ERROR_BADSTATE +
          " Unable to close all channels associated with this session"));
      });
    });
  },

  get reader() {
    return this._reader.__DOM_IMPL__;
  },

  get isClosed() {
    return this._isClosed;
  },

  set isClosed(isClosed) {
    this._isClosed = isClosed;
  }
};

/**
 * Instance of 'SEChannelImpl' object represent an ISO/IEC 7816-4 specification
 * channel opened to a secure element. It can be either a logical channel
 * or basic channel.
 */
function SEChannelImpl() {}

SEChannelImpl.prototype = {
  _window: null,

  _channelToken: null,

  _isClosed: false,

  _session: null,

  openResponse: [],

  type: null,

  classID: Components.ID("{181ebcf4-5164-4e28-99f2-877ec6fa83b9}"),
  contractID: "@mozilla.org/secureelement/channel;1",
  QueryInterface: XPCOMUtils.generateQI([]),

  _checkClosed: function _checkClosed() {
    if (this._isClosed) {
      throw new Error(SE.ERROR_BADSTATE + " Channel Already Closed!");
    }
  },

  // Chrome-only function
  onClose: function onClose() {
    this._isClosed = true;
    // Notify the parent
    this._session.onChannelClose(this.__DOM_IMPL__);
  },

  initialize: function initialize(win, channelToken, isBasicChannel,
                                  openResponse, sessionCtx) {
    this._window = win;
    // Update the 'channel token' that identifies and represents this
    // instance of the object
    this._channelToken = channelToken;
    // Update 'session' obj
    this._session = sessionCtx;
    this.openResponse = Cu.cloneInto(new Uint8Array(openResponse), win);
    this.type = isBasicChannel ? "basic" : "logical";
  },

  transmit: function transmit(command) {
    // TODO remove this once it will be possible to have a non-optional dict
    // in the WebIDL
    if (!command) {
      return PromiseHelpers.rejectWithSEError(SE.ERROR_GENERIC +
        " SECommand dict must be defined");
    }

    this._checkClosed();

    let dataLen = command.data ? command.data.length : 0;
    if (dataLen > SE.MAX_APDU_LEN) {
      return PromiseHelpers.rejectWithSEError(SE.ERROR_GENERIC +
             " Command data length exceeds max limit - 255. " +
             " Extended APDU is not supported!");
    }

    if ((command.cla & 0x80 === 0) && ((command.cla & 0x60) !== 0x20)) {
      if (command.ins === SE.INS_MANAGE_CHANNEL) {
        return PromiseHelpers.rejectWithSEError(SE.ERROR_SECURITY +
               ", MANAGE CHANNEL command not permitted");
      }
      if ((command.ins === SE.INS_SELECT) && (command.p1 == 0x04)) {
        // SELECT by DF Name (p1=04) is not allowed
        return PromiseHelpers.rejectWithSEError(SE.ERROR_SECURITY +
               ", SELECT command not permitted");
      }
      debug("Attempting to transmit an ISO command");
    } else {
      debug("Attempting to transmit GlobalPlatform command");
    }

    return PromiseHelpers.createSEPromise((resolverId) => {
      /**
       * @params for 'SE:TransmitAPDU'
       *
       * resolverId  : Id that identifies this IPC request.
       * apdu        : Object containing APDU data
       * channelToken: Token that identifies the current channel over which
                       'c-apdu' is being sent.
       * appId       : Current appId obtained from 'Principal' obj
       */
      cpmm.sendAsyncMessage("SE:TransmitAPDU", {
        resolverId: resolverId,
        apdu: command,
        channelToken: this._channelToken,
        appId: this._window.document.nodePrincipal.appId
      });
    }, this);
  },

  close: function close() {
    this._checkClosed();

    return PromiseHelpers.createSEPromise((resolverId) => {
      /**
       * @params for 'SE:CloseChannel'
       *
       * resolverId  : Id that identifies this IPC request.
       * channelToken: Token that identifies the current channel over which
                       'c-apdu' is being sent.
       * appId       : Current appId obtained from 'Principal' obj
       */
      cpmm.sendAsyncMessage("SE:CloseChannel", {
        resolverId: resolverId,
        channelToken: this._channelToken,
        appId: this._window.document.nodePrincipal.appId
      });
    }, this);
  },

  get session() {
    return this._session.__DOM_IMPL__;
  },

  get isClosed() {
    return this._isClosed;
  },

  set isClosed(isClosed) {
    this._isClosed = isClosed;
  }
};

function SEResponseImpl() {}

SEResponseImpl.prototype = {
  sw1: 0x00,

  sw2: 0x00,

  data: null,

  _channel: null,

  classID: Components.ID("{58bc6c7b-686c-47cc-8867-578a6ed23f4e}"),
  contractID: "@mozilla.org/secureelement/response;1",
  QueryInterface: XPCOMUtils.generateQI([]),

  initialize: function initialize(sw1, sw2, response, channelCtx) {
    // Update the status bytes
    this.sw1 = sw1;
    this.sw2 = sw2;
    this.data = response ? response.slice(0) : null;
    // Update the channel obj
    this._channel = channelCtx;
  },

  get channel() {
    return this._channel.__DOM_IMPL__;
  }
};

/**
 * SEManagerImpl
 */
function SEManagerImpl() {}

SEManagerImpl.prototype = {
  __proto__: DOMRequestIpcHelper.prototype,

  _window: null,

  classID: Components.ID("{4a8b6ec0-4674-11e4-916c-0800200c9a66}"),
  contractID: "@mozilla.org/secureelement/manager;1",
  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsIDOMGlobalPropertyInitializer,
    Ci.nsISupportsWeakReference,
    Ci.nsIObserver
  ]),

  init: function init(win) {
    this._window = win;
    PromiseHelpers = new PromiseHelpersSubclass(this._window);

    // Add the messages to be listened to.
    const messages = ["SE:GetSEReadersResolved",
                      "SE:OpenChannelResolved",
                      "SE:CloseChannelResolved",
                      "SE:TransmitAPDUResolved",
                      "SE:GetSEReadersRejected",
                      "SE:OpenChannelRejected",
                      "SE:CloseChannelRejected",
                      "SE:TransmitAPDURejected"];

    this.initDOMRequestHelper(win, messages);
  },

  // This function will be called from DOMRequestIPCHelper.
  uninit: function uninit() {
    // All requests that are still pending need to be invalidated
    // because the context is no longer valid.
    this.forEachPromiseResolver((k) => {
      this.takePromiseResolver(k).reject("Window Context got destroyed!");
    });
    PromiseHelpers = null;
    this._window = null;
  },

  getSEReaders: function getSEReaders() {
    return PromiseHelpers.createSEPromise((resolverId) => {
      /**
       * @params for 'SE:GetSEReaders'
       *
       * resolverId  : Id that identifies this IPC request.
       * appId       : Current appId obtained from 'Principal' obj
       */
      cpmm.sendAsyncMessage("SE:GetSEReaders", {
        resolverId: resolverId,
        appId: this._window.document.nodePrincipal.appId
      });
    });
  },

  receiveMessage: function receiveMessage(message) {
    let result = message.data.result;
    let chromeObj = null;
    let contentObj = null;
    let resolver = null;
    let context = null;

    let promiseResolver = PromiseHelpers.takePromise(result.resolverId);
    if (promiseResolver) {
      resolver = promiseResolver.resolver;
      // This 'context' is the instance that originated this IPC message.
      context = promiseResolver.context;
    }

    debug("receiveMessage(): " + message.name);
    switch (message.name) {
      case "SE:GetSEReadersResolved":
        let readers = new this._window.Array();
        for (let i = 0; i < result.readerTypes.length; i++) {
          chromeObj = new SEReaderImpl();
          chromeObj.initialize(this._window, result.readerTypes[i]);
          contentObj = this._window.SEReader._create(this._window, chromeObj);
          readers.push(contentObj);
        }
        resolver.resolve(readers);
        break;
      case "SE:OpenChannelResolved":
        chromeObj = new SEChannelImpl();
        chromeObj.initialize(this._window,
                             result.channelToken,
                             result.isBasicChannel,
                             result.openResponse,
                             context);
        contentObj = this._window.SEChannel._create(this._window, chromeObj);
        if (context) {
          // Notify context's handler with SEChannel instance
          context.onChannelOpen(contentObj);
        }
        resolver.resolve(contentObj);
        break;
      case "SE:TransmitAPDUResolved":
        chromeObj = new SEResponseImpl();
        chromeObj.initialize(result.sw1,
                             result.sw2,
                             result.response,
                             context);
        contentObj = this._window.SEResponse._create(this._window, chromeObj);
        resolver.resolve(contentObj);
        break;
      case "SE:CloseChannelResolved":
        if (context) {
          // Notify context's onClose handler
          context.onClose();
        }
        resolver.resolve();
        break;
      case "SE:GetSEReadersRejected":
      case "SE:OpenChannelRejected":
      case "SE:CloseChannelRejected":
      case "SE:TransmitAPDURejected":
        let error = result.error || SE.ERROR_GENERIC;
        resolver.reject(error);
        break;
      default:
        debug("Could not find a handler for " + message.name);
        resolver.reject();
        break;
    }
  }
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
  SEResponseImpl, SEChannelImpl, SESessionImpl, SEReaderImpl, SEManagerImpl
]);
