/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

const {Cc: Cc, Ci: Ci, Cr: Cr, Cu: Cu} = SpecialPowers;

// Emulate Promise.jsm semantics.
Promise.defer = function() { return new Deferred(); }
function Deferred()  {
  this.promise = new Promise(function(resolve, reject) {
    this.resolve = resolve;
    this.reject = reject;
  }.bind(this));
  Object.freeze(this);
}

const PDU_DCS_CODING_GROUP_BITS          = 0xF0;
const PDU_DCS_MSG_CODING_7BITS_ALPHABET  = 0x00;
const PDU_DCS_MSG_CODING_8BITS_ALPHABET  = 0x04;
const PDU_DCS_MSG_CODING_16BITS_ALPHABET = 0x08;

const PDU_DCS_MSG_CLASS_BITS             = 0x03;
const PDU_DCS_MSG_CLASS_NORMAL           = 0xFF;
const PDU_DCS_MSG_CLASS_0                = 0x00;
const PDU_DCS_MSG_CLASS_ME_SPECIFIC      = 0x01;
const PDU_DCS_MSG_CLASS_SIM_SPECIFIC     = 0x02;
const PDU_DCS_MSG_CLASS_TE_SPECIFIC      = 0x03;
const PDU_DCS_MSG_CLASS_USER_1           = 0x04;
const PDU_DCS_MSG_CLASS_USER_2           = 0x05;

const GECKO_SMS_MESSAGE_CLASSES = {};
GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]       = "normal";
GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]            = "class-0";
GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_ME_SPECIFIC]  = "class-1";
GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_SIM_SPECIFIC] = "class-2";
GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_TE_SPECIFIC]  = "class-3";
GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_1]       = "user-1";
GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_2]       = "user-2";

const CB_MESSAGE_SIZE_GSM  = 88;
const CB_MESSAGE_SIZE_ETWS = 56;

const CB_UMTS_MESSAGE_TYPE_CBS = 1;
const CB_UMTS_MESSAGE_PAGE_SIZE = 82;

const CB_GSM_MESSAGEID_ETWS_BEGIN = 0x1100;
const CB_GSM_MESSAGEID_ETWS_END   = 0x1107;

const CB_GSM_GEOGRAPHICAL_SCOPE_NAMES = [
  "cell-immediate",
  "plmn",
  "location-area",
  "cell"
];

const CB_ETWS_WARNING_TYPE_NAMES = [
  "earthquake",
  "tsunami",
  "earthquake-tsunami",
  "test",
  "other"
];

const CB_DCS_LANG_GROUP_1 = [
  "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi",
  "no", "el", "tr", "hu", "pl", null
];
const CB_DCS_LANG_GROUP_2 = [
  "cs", "he", "ar", "ru", "is", null, null, null, null, null,
  null, null, null, null, null, null
];

const CB_MAX_CONTENT_PER_PAGE_7BIT = Math.floor((CB_MESSAGE_SIZE_GSM - 6) * 8 / 7);
const CB_MAX_CONTENT_PER_PAGE_UCS2 = Math.floor((CB_MESSAGE_SIZE_GSM - 6) / 2);

const DUMMY_BODY_7BITS = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
                       + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
                       + "@@@@@@@@@@@@@"; // 93 ascii chars.
const DUMMY_BODY_7BITS_IND = DUMMY_BODY_7BITS.substr(3);
const DUMMY_BODY_UCS2 = "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                      + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                      + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                      + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                      + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                      + "\u0000"; // 41 unicode chars.
const DUMMY_BODY_UCS2_IND = DUMMY_BODY_UCS2.substr(1);

is(DUMMY_BODY_7BITS.length,     CB_MAX_CONTENT_PER_PAGE_7BIT,     "DUMMY_BODY_7BITS.length");
is(DUMMY_BODY_7BITS_IND.length, CB_MAX_CONTENT_PER_PAGE_7BIT - 3, "DUMMY_BODY_7BITS_IND.length");
is(DUMMY_BODY_UCS2.length,      CB_MAX_CONTENT_PER_PAGE_UCS2,     "DUMMY_BODY_UCS2.length");
is(DUMMY_BODY_UCS2_IND.length,  CB_MAX_CONTENT_PER_PAGE_UCS2 - 1, "DUMMY_BODY_UCS2_IND.length");

/**
 * Compose input number into specified number of semi-octets.
 *
 * @param: aNum
 *         The number to be converted.
 * @param: aNumSemiOctets
 *         Number of semi-octects to be composed to.
 *
 * @return The composed Hex String.
 */
function buildHexStr(aNum, aNumSemiOctets) {
  let str = aNum.toString(16);
  ok(str.length <= aNumSemiOctets);
  while (str.length < aNumSemiOctets) {
    str = "0" + str;
  }
  return str;
}

/**
 * Helper function to decode the given DCS into encoding type, language,
 * language indicator and message class.
 *
 * @param: aDcs
 *         The DCS to be decoded.
 *
 * @return [encoding, language, hasLanguageIndicator,
 *          GECKO_SMS_MESSAGE_CLASSES[messageClass]]
 */
function decodeGsmDataCodingScheme(aDcs) {
  let language = null;
  let hasLanguageIndicator = false;
  let encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET;
  let messageClass = PDU_DCS_MSG_CLASS_NORMAL;

  switch (aDcs & PDU_DCS_CODING_GROUP_BITS) {
    case 0x00: // 0000
      language = CB_DCS_LANG_GROUP_1[aDcs & 0x0F];
      break;

    case 0x10: // 0001
      switch (aDcs & 0x0F) {
        case 0x00:
          hasLanguageIndicator = true;
          break;
        case 0x01:
          encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET;
          hasLanguageIndicator = true;
          break;
      }
      break;

    case 0x20: // 0010
      language = CB_DCS_LANG_GROUP_2[aDcs & 0x0F];
      break;

    case 0x40: // 01xx
    case 0x50:
    //case 0x60:
    //case 0x70:
    case 0x90: // 1001
      encoding = (aDcs & 0x0C);
      if (encoding == 0x0C) {
        encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET;
      }
      messageClass = (aDcs & PDU_DCS_MSG_CLASS_BITS);
      break;

    case 0xF0:
      encoding = (aDcs & 0x04) ? PDU_DCS_MSG_CODING_8BITS_ALPHABET
                              : PDU_DCS_MSG_CODING_7BITS_ALPHABET;
      switch(aDcs & PDU_DCS_MSG_CLASS_BITS) {
        case 0x01: messageClass = PDU_DCS_MSG_CLASS_USER_1; break;
        case 0x02: messageClass = PDU_DCS_MSG_CLASS_USER_2; break;
        case 0x03: messageClass = PDU_DCS_MSG_CLASS_TE_SPECIFIC; break;
      }
      break;

    case 0x30: // 0011 (Reserved)
    case 0x80: // 1000 (Reserved)
    case 0xA0: // 1010..1100 (Reserved)
    case 0xB0:
    case 0xC0:
      break;
    default:
      throw new Error("Unsupported CBS data coding scheme: " + aDcs);
  }

  return [encoding, language, hasLanguageIndicator,
          GECKO_SMS_MESSAGE_CLASSES[messageClass]];
}

/**
 * Push required permissions and test if |navigator.mozCellBroadcast| exists.
 * Resolve if it does, reject otherwise.
 *
 * Fulfill params:
 *   cbManager -- an reference to navigator.mozCellBroadcast.
 *
 * Reject params: (none)
 *
 * @return A deferred promise.
 */
var cbManager;
function ensureCellBroadcast() {
  let deferred = Promise.defer();

  let permissions = [{
    "type": "cellbroadcast",
    "allow": 1,
    "context": document,
  }];
  SpecialPowers.pushPermissions(permissions, function() {
    ok(true, "permissions pushed: " + JSON.stringify(permissions));

    // Permission changes can't change existing Navigator.prototype
    // objects, so grab our objects from a new Navigator.
    let workingFrame = document.createElement("iframe");
    workingFrame.addEventListener("load", function load() {
      workingFrame.removeEventListener("load", load);

      cbManager = workingFrame.contentWindow.navigator.mozCellBroadcast;

      if (cbManager) {
        log("navigator.mozCellBroadcast is instance of " + cbManager.constructor);
      } else {
        log("navigator.mozCellBroadcast is undefined.");
      }

      if (cbManager instanceof window.MozCellBroadcast) {
        deferred.resolve(cbManager);
      } else {
        deferred.reject();
      }
    });

    document.body.appendChild(workingFrame);
  });

  return deferred.promise;
}

/**
 * Send emulator command with safe guard.
 *
 * We should only call |finish()| after all emulator command transactions
 * end, so here comes with the pending counter.  Resolve when the emulator
 * gives positive response, and reject otherwise.
 *
 * Fulfill params:
 *   result -- an array of emulator response lines.
 *
 * Reject params:
 *   result -- an array of emulator response lines.
 *
 * @return A deferred promise.
 */
var pendingEmulatorCmdCount = 0;
function runEmulatorCmdSafe(aCommand) {
  let deferred = Promise.defer();

  ++pendingEmulatorCmdCount;
  runEmulatorCmd(aCommand, function(aResult) {
    --pendingEmulatorCmdCount;

    ok(true, "Emulator response: " + JSON.stringify(aResult));
    if (Array.isArray(aResult) && aResult[aResult.length - 1] === "OK") {
      deferred.resolve(aResult);
    } else {
      deferred.reject(aResult);
    }
  });

  return deferred.promise;
}

/**
 * Send raw CBS PDU to emulator.
 *
 * @param: aPdu
 *         A hex string representing the whole CBS PDU.
 *
 * Fulfill params:
 *   result -- an array of emulator response lines.
 *
 * Reject params:
 *   result -- an array of emulator response lines.
 *
 * @return A deferred promise.
 */
function sendRawCbsToEmulator(aPdu) {
  let command = "cbs pdu " + aPdu;
  return runEmulatorCmdSafe(command);
}

/**
 * Wait for one named Cellbroadcast event.
 *
 * Resolve if that named event occurs.  Never reject.
 *
 * Fulfill params: the DOMEvent passed.
 *
 * @param aEventName
 *        A string event name.
 *
 * @return A deferred promise.
 */
function waitForManagerEvent(aEventName) {
  let deferred = Promise.defer();

  cbManager.addEventListener(aEventName, function onevent(aEvent) {
    cbManager.removeEventListener(aEventName, onevent);

    ok(true, "Cellbroadcast event '" + aEventName + "' got.");
    deferred.resolve(aEvent);
  });

  return deferred.promise;
}

/**
 * Send multiple raw CB PDU to emulator and wait
 *
 * @param: aPdus
 *         A array of hex strings. Each represents a CB PDU.
 *         These PDUs are expected to be concatenated into single CB Message.
 *
 * Fulfill params:
 *   result -- array of resolved Promise, where
 *             result[0].message representing the received message.
 *             result[1-n] represents the response of sent emulator command.
 *
 * Reject params:
 *   result -- an array of emulator response lines.
 *
 * @return A deferred promise.
 */
function sendMultipleRawCbsToEmulatorAndWait(aPdus) {
  let promises = [];

  promises.push(waitForManagerEvent("received"));
  for (let pdu of aPdus) {
    promises.push(sendRawCbsToEmulator(pdu));
  }

  return Promise.all(promises).then(aResults => aResults[0].message);
}

/**
 * Wait for pending emulator transactions and call |finish()|.
 */
function cleanUp() {
  // Use ok here so that we have at least one test run.
  ok(true, ":: CLEANING UP ::");

  waitFor(finish, function() {
    return pendingEmulatorCmdCount === 0;
  });
}

/**
 * Switch modem for receving upcoming emulator commands.
 *
 * @param: aServiceId
 *         The id of the modem to be switched to.
 *
 * Fulfill params:
 *   result -- an array of emulator response lines.
 *
 * Reject params:
 *   result -- an array of emulator response lines.
 *
 * @return A deferred promise.
 */
function selectModem(aServiceId) {
  let command = "mux modem " + aServiceId;
  return runEmulatorCmdSafe(command);
}

/**
 * Helper to run the test case only needed in Multi-SIM environment.
 *
 * @param  aTest
 *         A function which will be invoked w/o parameter.
 * @return a Promise object.
 */
function runIfMultiSIM(aTest) {
  let numRIL;
  try {
    numRIL = SpecialPowers.getIntPref("ril.numRadioInterfaces");
  } catch (ex) {
    numRIL = 1;  // Pref not set.
  }

  if (numRIL > 1) {
    return aTest();
  } else {
    log("Not a Multi-SIM environment. Test is skipped.");
    return Promise.resolve();
  }
}

/**
 * Common test routine helper for cell broadcast tests.
 *
 * This function ensures global |cbManager| variable is available during the
 * process and performs clean-ups as well.
 *
 * @param aTestCaseMain
 *        A function that takes no parameter.
 */
function startTestCommon(aTestCaseMain) {
  Promise.resolve()
         .then(ensureCellBroadcast)
         .then(aTestCaseMain)
         .then(cleanUp, function() {
           ok(false, 'promise rejects during test.');
           cleanUp();
         });
}
