// ***********************************
// * Global variables
// ***********************************
const kIsWin = navigator.platform.indexOf("Win") == 0;

// Bit value for the keyboard events
const kKeyDown  = 0x01;
const kKeyPress = 0x02;
const kKeyUp    = 0x04;

// Pair the event name to its bit value
const kEventCode = {
  'keydown'   : kKeyDown,
  'keypress'  : kKeyPress,
  'keyup'     : kKeyUp
};

// Holding the current test case's infomation:
var gCurrentTest;

// The current used input method of this test
var gInputMethod;

// ***********************************
// * Utilities
// ***********************************
function addKeyEventListeners(eventTarget, handler)
{
  Object.keys(kEventCode).forEach(function(type) {
    eventTarget.addEventListener(type, handler);
  });
}

function eventToCode(type)
{
  return kEventCode[type];
}

// To test key events that will be generated by input method here,
// we need to convert alphabets to native key code.
// (Our input method for testing will handle alphabets)
// On the other hand, to test key events that will not be generated by IME,
// we use 0-9 for such case in our testing.
function guessNativeKeyCode(key)
{
  let nativeCodeName = (kIsWin)? 'WIN_VK_' : 'MAC_VK_ANSI_';
  if (/^[A-Z]$/.test(key)) {
    nativeCodeName += key;
  } else if (/^[a-z]$/.test(key)) {
    nativeCodeName += key.toUpperCase();
  } else if (/^[0-9]$/.test(key)) {
    nativeCodeName += key.toString();
  } else {
    return 0;
  }

  return eval(nativeCodeName);
}

// ***********************************
// * Frame loader and frame scripts
// ***********************************
function frameScript()
{
  function handler(e) {
    sendAsyncMessage("forwardevent", { type: e.type, key: e.key });
  }
  function notifyFinish(e) {
    if (e.type != 'keyup') return;
    sendAsyncMessage("finish");
  }
  let input = content.document.getElementById('test-input');
  input.addEventListener('keydown', handler);
  input.addEventListener('keypress', handler);
  input.addEventListener('keyup', handler);
  input.addEventListener('keyup', notifyFinish);
}

function loadTestFrame(goNext) {
  let iframe = document.createElement('iframe');
  iframe.src = 'file_test_empty_app.html';
  iframe.setAttribute('mozbrowser', true);

  iframe.addEventListener("mozbrowserloadend", function onloadend() {
    iframe.removeEventListener("mozbrowserloadend", onloadend);
    iframe.focus();
    var mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
    mm.addMessageListener("forwardevent", function(msg) {
      inputtextEventReceiver(msg.json);
    });
    mm.addMessageListener("finish", function(msg) {
      if(goNext) {
        goNext();
      }
    });
    mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", false);
    return;
  });

  document.body.appendChild(iframe);
}

// ***********************************
// * Event firer and listeners
// ***********************************
function fireEvent(callback)
{
  let key = gCurrentTest.key;
  synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, guessNativeKeyCode(key), {},
                      key, key, (callback) ? callback : null);
}

function hardwareEventReceiver(evt)
{
  if (!gCurrentTest) {
    return;
  }
  gCurrentTest.hardwareinput.receivedEvents |= eventToCode(evt.type);
  gCurrentTest.hardwareinput.receivedKeys += evt.key;
}

function inputtextEventReceiver(evt)
{
  if (!gCurrentTest) {
    return;
  }
  gCurrentTest.inputtext.receivedEvents |= eventToCode(evt.type);
  gCurrentTest.inputtext.receivedKeys += evt.key;
}

// ***********************************
// * Event verifier
// ***********************************
function verifyResults(test)
{
  // Verify results received from inputcontent.hardwareinput
  is(test.hardwareinput.receivedEvents,
     test.hardwareinput.expectedEvents,
     "received events from inputcontent.hardwareinput are wrong");

  is(test.hardwareinput.receivedKeys,
     test.hardwareinput.expectedKeys,
     "received keys from inputcontent.hardwareinput are wrong");

  // Verify results received from actual input text
  is(test.inputtext.receivedEvents,
     test.inputtext.expectedEvents,
     "received events from input text are wrong");

  is(test.inputtext.receivedKeys,
     test.inputtext.expectedKeys,
     "received keys from input text are wrong");
}

function areEventsSame(test)
{
  return (test.hardwareinput.receivedEvents ==
          test.hardwareinput.expectedEvents) &&
         (test.inputtext.receivedEvents ==
          test.inputtext.expectedEvents);
}

// ***********************************
// * Input Method
// ***********************************
// The method input used in this test
// only handles alphabets
function InputMethod(inputContext)
{
  this._inputContext = inputContext;
  this.init();
}

InputMethod.prototype = {
  init: function im_init() {
    this._setKepMap();
  },

  handler: function im_handler(evt) {
    // Ignore the key if the event is defaultPrevented
    if (evt.defaultPrevented) {
      return;
    }

    // Finish if there is no _inputContext
    if (!this._inputContext) {
      return;
    }

    // Generate the keyDict for inputcontext.keydown/keyup
    let keyDict = this._generateKeyDict(evt);

    // Ignore the key if IME doesn't want to handle it
    if (!keyDict) {
      return;
    }

    // Call preventDefault if the key will be handled.
    evt.preventDefault();

    // Call inputcontext.keydown/keyup
    this._inputContext[evt.type](keyDict);
  },

  mapKey: function im_keymapping(key) {
    if (!this._mappingTable) {
      return;
    }
    return this._mappingTable[key];
  },

  _setKepMap: function im_setKeyMap() {
    // A table to map characters:
    // {
    //   'A': 'B'
    //   'a': 'b'
    //   'B': 'C'
    //   'b': 'c'
    //   ..
    //   ..
    //   'Z': 'A',
    //   'z': 'a',
    // }
    this._mappingTable = {};

    let rotation = 1;

    for (let i = 0 ; i < 26 ; i++) {
      // Convert 'A' to 'B', 'B' to 'C', ..., 'Z' to 'A'
      this._mappingTable[String.fromCharCode(i + 'A'.charCodeAt(0))] =
        String.fromCharCode((i+rotation)%26 + 'A'.charCodeAt(0));

      // Convert 'a' to 'b', 'b' to 'c', ..., 'z' to 'a'
      this._mappingTable[String.fromCharCode(i + 'a'.charCodeAt(0))] =
        String.fromCharCode((i+rotation)%26 + 'a'.charCodeAt(0));
    }
  },

  _generateKeyDict: function im_generateKeyDict(evt) {

    let mappedKey = this.mapKey(evt.key);

    if (!mappedKey) {
      return;
    }

    let keyDict = {
      key: mappedKey,
      code: this._guessCodeFromKey(mappedKey),
      repeat: evt.repeat,
    };

    return keyDict;
  },

  _guessCodeFromKey: function im_guessCodeFromKey(key) {
    if (/^[A-Z]$/.test(key)) {
      return "Key" + key;
    } else if (/^[a-z]$/.test(key)) {
      return "Key" + key.toUpperCase();
    } else if (/^[0-9]$/.test(key)) {
      return "Digit" + key.toString();
    } else {
      return 0;
    }
  },
};