CardJs.prototype.constructor = CardJs ;

/**
 * @class CardJs
 *
 * @author Colin Stannard
 *
 * @param elem
 * @constructor
 */
function CardJs(elem) {
  this.elem = jQuery(elem);

  this.captureName = this.elem.data("capture-name") ? this.elem.data("capture-name") : false;
  this.iconColour = this.elem.data("icon-colour") ? this.elem.data("icon-colour") : false;
  this.stripe = this.elem.data("stripe") ? this.elem.data("stripe") : false;

  if(this.stripe) {
    this.captureName = false;
  }

  // Initialise
  this.initCardNumberInput();
  this.initNameInput();
  this.initExpiryMonthInput();
  this.initExpiryYearInput();
  this.initCvcInput();

  this.elem.empty();

  // Setup display
  this.setupCardNumberInput();
  this.setupNameInput();
  this.setupExpiryInput();
  this.setupCvcInput();

  // Set icon colour
  if(this.iconColour) {
    this.setIconColour(this.iconColour);
  }

  // --- --- --- --- --- --- --- --- --- ---

  this.refreshCreditCardTypeIcon();

}

CardJs.KEYS = {
  "0" : 48,
  "9" : 57,
  "NUMPAD_0" : 96,
  "NUMPAD_9" : 105,
  "DELETE" : 46,
  "BACKSPACE" : 8,
  "ARROW_LEFT" : 37,
  "ARROW_RIGHT" : 39,
  "ARROW_UP" : 38,
  "ARROW_DOWN" : 40,
  "HOME" : 36,
  "END" : 35,
  "TAB" : 9,
  "A" : 65,
  "X" : 88,
  "C" : 67,
  "V" : 86
};

CardJs.CREDIT_CARD_NUMBER_DEFAULT_MASK    = "XXXX XXXX XXXX XXXX";
CardJs.CREDIT_CARD_NUMBER_VISA_MASK       = "XXXX XXXX XXXX XXXX";
CardJs.CREDIT_CARD_NUMBER_MASTERCARD_MASK = "XXXX XXXX XXXX XXXX";
CardJs.CREDIT_CARD_NUMBER_DISCOVER_MASK   = "XXXX XXXX XXXX XXXX";
CardJs.CREDIT_CARD_NUMBER_JCB_MASK        = "XXXX XXXX XXXX XXXX";
CardJs.CREDIT_CARD_NUMBER_AMEX_MASK       = "XXXX XXXXXX XXXXX";
CardJs.CREDIT_CARD_NUMBER_DINERS_MASK     = "XXXX XXXX XXXX XX";

CardJs.prototype.creditCardNumberMask = CardJs.CREDIT_CARD_NUMBER_DEFAULT_MASK;
CardJs.CREDIT_CARD_NUMBER_PLACEHOLDER = "Card number";
CardJs.NAME_PLACEHOLDER =  "Name on card";
CardJs.EXPIRY_MASK = "XX / XX";
CardJs.EXPIRY_PLACEHOLDER = "MM / YY";
CardJs.EXPIRY_USE_DROPDOWNS = false;
CardJs.EXPIRY_NUMBER_OF_YEARS = 10;
CardJs.CVC_MASK_3 = "XXX";
CardJs.CVC_MASK_4 = "XXXX";
CardJs.CVC_PLACEHOLDER =  "CVC";

CardJs.CREDIT_CARD_SVG = '<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
  'x="0px" y="3px" width="24px" height="17px" viewBox="0 0 216 146" enable-background="new 0 0 216 146" xml:space="preserve">' +
  '<g><path class="svg" d="M182.385,14.258c-2.553-2.553-5.621-3.829-9.205-3.829H42.821c-3.585,0-6.653,1.276-9.207,3.829' +
  'c-2.553,2.553-3.829,5.621-3.829,9.206v99.071c0,3.585,1.276,6.654,3.829,9.207c2.554,2.553,5.622,3.829,9.207,3.829H173.18' +
  'c3.584,0,6.652-1.276,9.205-3.829s3.83-5.622,3.83-9.207V23.464C186.215,19.879,184.938,16.811,182.385,14.258z M175.785,122.536' +
  'c0,0.707-0.258,1.317-0.773,1.834c-0.516,0.515-1.127,0.772-1.832,0.772H42.821c-0.706,0-1.317-0.258-1.833-0.773' +
  'c-0.516-0.518-0.774-1.127-0.774-1.834V73h135.571V122.536z M175.785,41.713H40.214v-18.25c0-0.706,0.257-1.316,0.774-1.833' +
  'c0.516-0.515,1.127-0.773,1.833-0.773H173.18c0.705,0,1.316,0.257,1.832,0.773c0.516,0.517,0.773,1.127,0.773,1.833V41.713z"/>' +
  '<rect class="svg" x="50.643" y="104.285" width="20.857" height="10.429"/>' +
  '<rect class="svg" x="81.929" y="104.285" width="31.286" height="10.429"/>'+
  '</g></svg>';

CardJs.LOCK_SVG = '<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
  'x="0px" y="3px" width="24px" height="17px" viewBox="0 0 216 146" enable-background="new 0 0 216 146" xml:space="preserve">' +
  '<path class="svg" d="M152.646,70.067c-1.521-1.521-3.367-2.281-5.541-2.281H144.5V52.142c0-9.994-3.585-18.575-10.754-25.745' +
  'c-7.17-7.17-15.751-10.755-25.746-10.755s-18.577,3.585-25.746,10.755C75.084,33.567,71.5,42.148,71.5,52.142v15.644' +
  'h-2.607c-2.172,0-4.019,0.76-5.54,2.281c-1.521,1.52-2.281,3.367-2.281,5.541v46.929c0,2.172,0.76,4.019,2.281,5.54' +
  'c1.521,1.52,3.368,2.281,5.54,2.281h78.214c2.174,0,4.02-0.76,5.541-2.281c1.52-1.521,2.281-3.368,2.281-5.54V75.607' +
  'C154.93,73.435,154.168,71.588,152.646,70.067z M128.857,67.786H87.143V52.142c0-5.757,2.037-10.673,6.111-14.746' +
  'c4.074-4.074,8.989-6.11,14.747-6.11s10.673,2.036,14.746,6.11c4.073,4.073,6.11,8.989,6.11,14.746V67.786z"/></svg>';

CardJs.CALENDAR_SVG = '<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
  'x="0px" y="4px" width="24px" height="16px" viewBox="0 0 216 146" enable-background="new 0 0 216 146" xml:space="preserve">' +
  '<path class="svg" d="M172.691,23.953c-2.062-2.064-4.508-3.096-7.332-3.096h-10.428v-7.822c0-3.584-1.277-6.653-3.83-9.206' +
  'c-2.554-2.553-5.621-3.83-9.207-3.83h-5.213c-3.586,0-6.654,1.277-9.207,3.83c-2.554,2.553-3.83,5.622-3.83,9.206' +
  'v7.822H92.359v-7.822c0-3.584-1.277-6.653-3.83-9.206c-2.553-2.553-5.622-3.83-9.207-3.83h-5.214c-3.585,0-6.654,' +
  '1.277-9.207,3.83c-2.553,2.553-3.83,5.622-3.83,9.206v7.822H50.643c-2.825,0-5.269,1.032-7.333,3.096s-3.096,' +
  '4.509-3.096,7.333v104.287c0,2.823,1.032,5.267,3.096,7.332c2.064,2.064,4.508,3.096,7.333,3.096h114.714c2.824,0,' +
  '5.27-1.032,7.332-3.096c2.064-2.064,3.096-4.509,3.096-7.332V31.286C175.785,28.461,174.754,26.017,172.691,23.953z ' +
  'M134.073,13.036c0-0.761,0.243-1.386,0.731-1.874c0.488-0.488,1.113-0.733,1.875-0.733h5.213c0.762,0,1.385,0.244,' +
  '1.875,0.733c0.488,0.489,0.732,1.114,0.732,1.874V36.5c0,0.761-0.244,1.385-0.732,1.874c-0.49,0.488-1.113,' +
  '0.733-1.875,0.733h-5.213c-0.762,0-1.387-0.244-1.875-0.733s-0.731-1.113-0.731-1.874V13.036z M71.501,13.036' +
  'c0-0.761,0.244-1.386,0.733-1.874c0.489-0.488,1.113-0.733,1.874-0.733h5.214c0.761,0,1.386,0.244,1.874,0.733' +
  'c0.488,0.489,0.733,1.114,0.733,1.874V36.5c0,0.761-0.244,1.386-0.733,1.874c-0.489,0.488-1.113,0.733-1.874,0.733' +
  'h-5.214c-0.761,0-1.386-0.244-1.874-0.733c-0.488-0.489-0.733-1.113-0.733-1.874V13.036z M165.357,135.572H50.643' +
  'V52.143h114.714V135.572z"/></svg>';

CardJs.USER_SVG = '<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
  'x="0px" y="4px" width="24px" height="16px" viewBox="0 0 216 146" enable-background="new 0 0 216 146" xml:space="preserve">' +
  '<g><path class="svg" d="M107.999,73c8.638,0,16.011-3.056,22.12-9.166c6.111-6.11,9.166-13.483,9.166-22.12c0-' +
  '8.636-3.055-16.009-9.166-22.12c-6.11-6.11-13.484-9.165-22.12-9.165c-8.636,0-16.01,3.055-22.12,9.165c-6.111,' +
  '6.111-9.166,13.484-9.166,22.12c0,8.637,3.055,16.01,9.166,22.12C91.99,69.944,99.363,73,107.999,73z"/>' +
  '<path class="svg" d="M165.07,106.037c-0.191-2.743-0.571-5.703-1.141-8.881c-0.57-3.178-1.291-6.124-2.16-8.84' +
  'c-0.869-2.715-2.037-5.363-3.504-7.943c-1.466-2.58-3.15-4.78-5.052-6.6s-4.223-3.272-6.965-4.358c-2.744-1.086' +
  '-5.772-1.63-9.085-1.63c-0.489,0-1.63,0.584-3.422,1.752s-3.815,2.472-6.069,3.911c-2.254,1.438-5.188,2.743-8.' +
  '799,3.909c-3.612,1.168-7.237,1.752-10.877,1.752c-3.639,0-7.264-0.584-10.876-1.752c-3.611-1.166-6.545-2.471-' +
  '8.799-3.909c-2.254-1.439-4.277-2.743-6.069-3.911c-1.793-1.168-2.933-1.752-3.422-1.752c-3.313,0-6.341,0.544-' +
  '9.084,1.63s-5.065,2.539-6.966,4.358c-1.901,1.82-3.585,4.02-5.051,6.6s-2.634,5.229-3.503,7.943c-0.869,2.716-' +
  '1.589,5.662-2.159,8.84c-0.571,3.178-0.951,6.137-1.141,8.881c-0.19,2.744-0.285,5.554-0.285,8.433c0,6.517,1.9' +
  '83,11.664,5.948,15.439c3.965,3.774,9.234,5.661,15.806,5.661h71.208c6.572,0,11.84-1.887,15.806-5.661c3.966-3' +
  '.775,5.948-8.921,5.948-15.439C165.357,111.591,165.262,108.78,165.07,106.037z"/></g></svg>';

CardJs.MAIL_SVG = '<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' +
  'x="0px" y="4px" width="24px" height="16px" viewBox="0 0 216 146" enable-background="new 0 0 216 146" xml:space="preserve">' +
  '<path class="svg" d="M177.171,19.472c-2.553-2.553-5.622-3.829-9.206-3.829H48.036c-3.585,0-6.654,1.276-9.207,3.829C36.276,' +
  '22.025,35,25.094,35,28.679v88.644c0,3.585,1.276,6.652,3.829,9.205c2.553,2.555,5.622,3.83,9.207,3.83h119.929c3' +
  '.584,0,6.653-1.275,9.206-3.83c2.554-2.553,3.829-5.621,3.829-9.205V28.679C181,25.094,179.725,22.025,177.171,19' +
  '.472zM170.57,117.321c0,0.706-0.258,1.317-0.774,1.833s-1.127,0.773-1.832,0.773H48.035c-0.706,0-1.317-0.257-1.8' +
  '33-0.773c-0.516-0.516-0.774-1.127-0.774-1.833V54.75c1.738,1.955,3.612,3.748,5.622,5.377c14.557,11.189,26.126,' +
  '20.368,34.708,27.538c2.77,2.336,5.024,4.155,6.762,5.459s4.087,2.62,7.047,3.951s5.744,1.995,8.351,1.995H108h0.' +
  '081c2.606,0,5.392-0.664,8.351-1.995c2.961-1.331,5.311-2.647,7.049-3.951c1.737-1.304,3.992-3.123,6.762-5.459c8' +
  '.582-7.17,20.15-16.349,34.707-27.538c2.01-1.629,3.885-3.422,5.621-5.377V117.321z M170.57,30.797v0.896c0,3.204' +
  '-1.262,6.776-3.787,10.713c-2.525,3.938-5.256,7.075-8.188,9.41c-10.484,8.257-21.373,16.865-32.672,25.827c-0.32' +
  '6,0.271-1.277,1.073-2.852,2.403c-1.574,1.331-2.824,2.351-3.748,3.056c-0.924,0.707-2.131,1.562-3.625,2.566s-2.' +
  '865,1.752-4.114,2.24s-2.417,0.732-3.503,0.732H108h-0.082c-1.086,0-2.253-0.244-3.503-0.732c-1.249-0.488-2.621-' +
  '1.236-4.114-2.24c-1.493-1.004-2.702-1.859-3.625-2.566c-0.923-0.705-2.173-1.725-3.748-3.056c-1.575-1.33-2.526-' +
  '2.132-2.852-2.403c-11.297-8.962-22.187-17.57-32.67-25.827c-7.985-6.3-11.977-14.013-11.977-23.138c0-0.706,0.25' +
  '8-1.317,0.774-1.833c0.516-0.516,1.127-0.774,1.833-0.774h119.929c0.434,0.244,0.814,0.312,1.141,0.204c0.326-0.1' +
  '1,0.57,0.094,0.732,0.61c0.163,0.516,0.312,0.76,0.448,0.733c0.136-0.027,0.218,0.312,0.245,1.019c0.025,0.706,0.' +
  '039,1.061,0.039,1.061V30.797z"/></svg>';

CardJs.INFORMATION_SVG = '<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
  'x="0px" y="4px" width="24px" height="16px" viewBox="0 0 216 146" enable-background="new 0 0 216 146" xml:space="preserve">' +
  '<g><path class="svg" d="M97.571,41.714h20.859c1.411,0,2.633-0.516,3.666-1.548c1.031-1.031,1.547-2.254,1.547-' +
  '3.666V20.857c0-1.412-0.516-2.634-1.549-3.667c-1.031-1.031-2.254-1.548-3.666-1.548H97.571c-1.412,0-2.634,0.51' +
  '7-3.666,1.548c-1.032,1.032-1.548,2.255-1.548,3.667V36.5c0,1.412,0.516,2.635,1.548,3.666C94.937,41.198,96.159' +
  ',41.714,97.571,41.714z"/><path class="svg" d="M132.523,111.048c-1.031-1.032-2.254-1.548-3.666-1.548h-5.215V6' +
  '2.571c0-1.412-0.516-2.634-1.547-3.666c-1.033-1.032-2.255-1.548-3.666-1.548H87.143c-1.412,0-2.634,0.516-3.666' +
  ',1.548c-1.032,1.032-1.548,2.254-1.548,3.666V73c0,1.412,0.516,2.635,1.548,3.666c1.032,1.032,2.254,1.548,3.666' +
  ',1.548h5.215V109.5h-5.215c-1.412,0-2.634,0.516-3.666,1.548c-1.032,1.032-1.548,2.254-1.548,3.666v10.429c0,1.4' +
  '12,0.516,2.635,1.548,3.668c1.032,1.03,2.254,1.547,3.666,1.547h41.714c1.412,0,2.634-0.517,3.666-1.547c1.031-1' +
  '.033,1.547-2.256,1.547-3.668v-10.429C134.07,113.302,133.557,112.08,132.523,111.048z"/></g></svg>';

/**
 * Get the key code from the given event.
 *
 * @param e
 * @returns {which|*|Object|which|which|string}
 */
CardJs.keyCodeFromEvent = function(e) {
  return e.which || e.keyCode;
};

/**
 * Get whether a command key (ctrl of mac cmd) is held down.
 *
 * @param e
 * @returns {boolean|metaKey|*|metaKey}
 */
CardJs.keyIsCommandFromEvent = function(e) {
  return e.ctrlKey || e.metaKey;
};

/**
 * Is the event a number key.
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsNumber = function(e) {
  return CardJs.keyIsTopNumber(e) || CardJs.keyIsKeypadNumber(e);
};

/**
 * Is the event a top keyboard number key.
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsTopNumber = function(e) {
  var keyCode = CardJs.keyCodeFromEvent(e);
  return keyCode >= CardJs.KEYS["0"] && keyCode <= CardJs.KEYS["9"];
};

/**
 * Is the event a keypad number key.
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsKeypadNumber = function(e) {
  var keyCode = CardJs.keyCodeFromEvent(e);
  return keyCode >= CardJs.KEYS["NUMPAD_0"] && keyCode <= CardJs.KEYS["NUMPAD_9"];
};

/**
 * Is the event a delete key.
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsDelete = function(e) {
  return CardJs.keyCodeFromEvent(e) == CardJs.KEYS["DELETE"];
};

/**
 * Is the event a backspace key.
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsBackspace = function(e) {
  return CardJs.keyCodeFromEvent(e) == CardJs.KEYS["BACKSPACE"];
};

/**
 * Is the event a deletion key (delete or backspace)
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsDeletion = function(e) {
  return CardJs.keyIsDelete(e) || CardJs.keyIsBackspace(e);
};

/**
 * Is the event an arrow key.
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsArrow = function(e) {
  var keyCode = CardJs.keyCodeFromEvent(e);
  return keyCode >= CardJs.KEYS["ARROW_LEFT"] && keyCode <= CardJs.KEYS["ARROW_DOWN"];
};

/**
 * Is the event a navigation key.
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsNavigation = function(e) {
  var keyCode = CardJs.keyCodeFromEvent(e);
  return keyCode == CardJs.KEYS["HOME"] || keyCode == CardJs.KEYS["END"];
};

/**
 * Is the event a keyboard command (copy, paste, cut, highlight all)
 *
 * @param e
 * @returns {boolean|metaKey|*|metaKey|boolean}
 */
CardJs.keyIsKeyboardCommand = function(e) {
  var keyCode = CardJs.keyCodeFromEvent(e);
  return CardJs.keyIsCommandFromEvent(e) &&
    (
      keyCode == CardJs.KEYS["A"] ||
      keyCode == CardJs.KEYS["X"] ||
      keyCode == CardJs.KEYS["C"] ||
      keyCode == CardJs.KEYS["V"]
    );
};

/**
 * Is the event the tab key?
 *
 * @param e
 * @returns {boolean}
 */
CardJs.keyIsTab = function(e) {
  return CardJs.keyCodeFromEvent(e) == CardJs.KEYS["TAB"];
};

/**
 * Copy all attributes of the source element to the destination element.
 *
 * @param source
 * @param destination
 */
CardJs.copyAllElementAttributes = function(source, destination) {
  $.each(source[0].attributes, function(idx, attr) {
    destination.attr(attr.nodeName, attr.nodeValue);
  });
};

/**
 * Strip all characters that are not in the range 0-9
 *
 * @param string
 * @returns {string}
 */
CardJs.numbersOnlyString = function(string) {
  var numbersOnlyString = "";
  for(var i = 0; i < string.length; i++) {
    var currentChar = string.charAt(i);
    var isValid = !isNaN(parseInt(currentChar));
    if(isValid) { numbersOnlyString += currentChar; }
  }
  return numbersOnlyString;
};

/**
 * Apply a format mask to the given string
 *
 * @param string
 * @param mask
 * @returns {string}
 */
CardJs.applyFormatMask = function(string, mask) {
  var formattedString = "";
  var numberPos = 0;
  for(var j = 0; j < mask.length; j++) {
    var currentMaskChar = mask[j];
    if(currentMaskChar == "X") {
      var digit = string.charAt(numberPos);
      if(!digit) {
        break;
      }
      formattedString += string.charAt(numberPos);
      numberPos++;
    } else {
      formattedString += currentMaskChar;
    }
  }
  return formattedString;
};

/**
 * Establish the type of a card from the number.
 *
 * @param number
 * @returns {string}
 */
CardJs.cardTypeFromNumber = function(number) {

  // Diners - Carte Blanche
  re = new RegExp("^30[0-5]");
  if (number.match(re) != null)
    return "Diners - Carte Blanche";

  // Diners
  re = new RegExp("^(30[6-9]|36|38)");
  if (number.match(re) != null)
    return "Diners";

  // JCB
  re = new RegExp("^35(2[89]|[3-8][0-9])");
  if (number.match(re) != null)
    return "JCB";

  // AMEX
  re = new RegExp("^3[47]");
  if (number.match(re) != null)
    return "AMEX";

  // Visa Electron
  re = new RegExp("^(4026|417500|4508|4844|491(3|7))");
  if (number.match(re) != null)
    return "Visa Electron";

  // Visa
  var re = new RegExp("^4");
  if (number.match(re) != null)
    return "Visa";

  // Mastercard
  re = new RegExp("^5[1-5]");
  if (number.match(re) != null)
    return "Mastercard";

  // Discover
  re = new RegExp("^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)");
  if (number.match(re) != null)
    return "Discover";

  return "";
};

/**
 * Get the caret start position of the given element.
 *
 * @param element
 * @returns {*}
 */
CardJs.caretStartPosition = function(element) {
  if(typeof element.selectionStart == "number") {
    return element.selectionStart;
  }
  return false;
};

/**
 * Gte the caret end position of the given element.
 *
 * @param element
 * @returns {*}
 */
CardJs.caretEndPosition = function(element) {
  if(typeof element.selectionEnd == "number") {
    return element.selectionEnd;
  }
  return false;
};

/**
 * Set the caret position of the given element.
 *
 * @param element
 * @param caretPos
 */
CardJs.setCaretPosition = function(element, caretPos) {
  if(element != null) {
    if(element.createTextRange) {
      var range = element.createTextRange();
      range.move('character', caretPos);
      range.select();
    } else {
      if(element.selectionStart) {
        element.focus();
        element.setSelectionRange(caretPos, caretPos);
      } else {
        element.focus();
      }
    }
  }
};

/**
 * Normalise the caret position for the given mask.
 *
 * @param mask
 * @param caretPosition
 * @returns {number}
 */
CardJs.normaliseCaretPosition = function(mask, caretPosition) {
  var numberPos = 0;
  if(caretPosition < 0 || caretPosition > mask.length) { return 0; }
  for(var i = 0; i < mask.length; i++) {
    if(i == caretPosition) { return numberPos; }
    if(mask[i] == "X") { numberPos++; }
  }
  return numberPos;
};

/**
 * Denormalise the caret position for the given mask.
 *
 * @param mask
 * @param caretPosition
 * @returns {*}
 */
CardJs.denormaliseCaretPosition = function(mask, caretPosition) {
  var numberPos = 0;
  if(caretPosition < 0 || caretPosition > mask.length) { return 0; }
  for(var i = 0; i < mask.length; i++) {
    if(numberPos == caretPosition) { return i; }
    if(mask[i] == "X") { numberPos++; }
  }
  return mask.length;
};

/**
 *
 *
 * @param e
 */
CardJs.filterNumberOnlyKey = function(e) {
  var isNumber = CardJs.keyIsNumber(e);
  var isDeletion = CardJs.keyIsDeletion(e);
  var isArrow = CardJs.keyIsArrow(e);
  var isNavigation = CardJs.keyIsNavigation(e);
  var isKeyboardCommand = CardJs.keyIsKeyboardCommand(e);
  var isTab = CardJs.keyIsTab(e);

  if(!isNumber && !isDeletion && !isArrow && !isNavigation && !isKeyboardCommand && !isTab) {
    e.preventDefault();
  }
};

/**
 *
 *
 * @param keyCode
 * @returns {*}
 */
CardJs.digitFromKeyCode = function(keyCode) {

  if(keyCode >= CardJs.KEYS["0"] && keyCode <= CardJs.KEYS["9"]) {
    return keyCode - CardJs.KEYS["0"];
  }

  if(keyCode >= CardJs.KEYS["NUMPAD_0"] && keyCode <= CardJs.KEYS["NUMPAD_9"]) {
    return keyCode - CardJs.KEYS["NUMPAD_0"];
  }

  return null;
};

/**
 *
 *
 * @param e
 * @param mask
 */
CardJs.handleMaskedNumberInputKey = function(e, mask) {
  CardJs.filterNumberOnlyKey(e);

  var keyCode = e.which || e.keyCode;

  var element = e.target;

  var caretStart = CardJs.caretStartPosition(element);
  var caretEnd = CardJs.caretEndPosition(element);

  // Calculate normalised caret position
  var normalisedStartCaretPosition = CardJs.normaliseCaretPosition(mask, caretStart);
  var normalisedEndCaretPosition = CardJs.normaliseCaretPosition(mask, caretEnd);

  var newCaretPosition = caretStart;

  var isNumber = CardJs.keyIsNumber(e);
  var isDelete = CardJs.keyIsDelete(e);
  var isBackspace = CardJs.keyIsBackspace(e);

  if (isNumber || isDelete || isBackspace) {
    e.preventDefault();
    var rawText = $(element).val();
    var numbersOnly = CardJs.numbersOnlyString(rawText);

    var digit = CardJs.digitFromKeyCode(keyCode);

    var rangeHighlighted = normalisedEndCaretPosition > normalisedStartCaretPosition;

    // Remove values highlighted (if highlighted)
    if (rangeHighlighted) {
      numbersOnly = (numbersOnly.slice(0, normalisedStartCaretPosition) + numbersOnly.slice(normalisedEndCaretPosition));
    }

    // Forward Action
    if (caretStart != mask.length) {

      // Insert number digit
      if (isNumber && rawText.length <= mask.length) {
        numbersOnly = (numbersOnly.slice(0, normalisedStartCaretPosition) + digit + numbersOnly.slice(normalisedStartCaretPosition));
        newCaretPosition = Math.max(
          CardJs.denormaliseCaretPosition(mask, normalisedStartCaretPosition + 1),
          CardJs.denormaliseCaretPosition(mask, normalisedStartCaretPosition + 2) - 1
        );
      }

      // Delete
      if (isDelete) {
        numbersOnly = (numbersOnly.slice(0, normalisedStartCaretPosition) + numbersOnly.slice(normalisedStartCaretPosition + 1));
      }

    }

    // Backward Action
    if (caretStart != 0) {

      // Backspace
      if(isBackspace && !rangeHighlighted) {
        numbersOnly = (numbersOnly.slice(0, normalisedStartCaretPosition - 1) + numbersOnly.slice(normalisedStartCaretPosition));
        newCaretPosition = CardJs.denormaliseCaretPosition(mask, normalisedStartCaretPosition - 1);
      }
    }

    $(element).val(CardJs.applyFormatMask(numbersOnly, mask));

    CardJs.setCaretPosition(element, newCaretPosition);
  }
};

/**
 *
 *
 * @param e
 * @param cardMask
 */
CardJs.handleCreditCardNumberKey = function(e, cardMask) {
  CardJs.handleMaskedNumberInputKey(e, cardMask);
};

CardJs.handleCreditCardNumberChange = function(e) {

};

CardJs.handleExpiryKey = function(e) {
  CardJs.handleMaskedNumberInputKey(e, CardJs.EXPIRY_MASK);
};

/**
 * Dispatch beacon ~ for future feature.
 */
//CardJs.dispatchBeacon = function() {
//  $.ajax({
//    type: "POST",
//    url: "https://cardjs.co.uk/beacon",
//    data: {
//      uri: window.location.href
//    },
//    crossDomain : true,
//    dataType: 'json'
//  });
//};

//window.onload = function() {
  //CardJs.dispatchBeacon();
//};

/**
 * Get the card number inputted.
 *
 * @returns {string}
 */
CardJs.prototype.getCardNumber = function() {
  return this.cardNumberInput.val();
};

/**
 * Get the type of the card number inputted.
 *
 * @returns {string}
 */
CardJs.prototype.getCardType = function() {
  return CardJs.cardTypeFromNumber(this.getCardNumber());
};

/**
 * Get the name inputted.
 *
 * @returns {string}
 */
CardJs.prototype.getName = function() {
  return this.nameInput.val();
};

/**
 * Get the expiry month inputted.
 *
 * @returns {string}
 */
CardJs.prototype.getExpiryMonth = function() {
  return this.expiryMonthInput.val();
};

/**
 * Get the expiry year inputted.
 *
 * @returns {string}
 */
CardJs.prototype.getExpiryYear = function() {
  return this.expiryYearInput.val();
};

/**
 * Get the CVC number inputted.
 *
 * @returns {number}
 */
CardJs.prototype.getCvc = function() {
  return this.cvcInput.val();
};

// --- --- --- --- --- --- --- --- --- --- ---

/**
 * Set the icon colour.
 *
 * @param colour
 */
CardJs.prototype.setIconColour = function(colour) {
  this.elem.find(".icon .svg").css({"fill": colour});
};

/**
 * Set the icon colour.
 *
 * @param colour
 */
CardJs.prototype.setIconColour = function(colour) {
  this.elem.find(".icon .svg").css({"fill": colour});
};

/**
 *
 */
CardJs.prototype.refreshCreditCardTypeIcon = function() {
  this.setCardTypeIconFromNumber(CardJs.numbersOnlyString(this.cardNumberInput.val()));
};

/**
 *
 */
CardJs.prototype.refreshCreditCardNumberFormat = function() {
  var numbersOnly = CardJs.numbersOnlyString($(this.cardNumberInput).val());
  var formattedNumber = CardJs.applyFormatMask(numbersOnly, this.creditCardNumberMask);
  $(this.cardNumberInput).val(formattedNumber);
};

/**
 *
 */
CardJs.prototype.refreshExpiryMonthYearInput = function() {
  var numbersOnly = CardJs.numbersOnlyString($(this.expiryMonthYearInput).val());
  var formattedNumber = CardJs.applyFormatMask(numbersOnly, CardJs.EXPIRY_MASK);
  $(this.expiryMonthYearInput).val(formattedNumber);
};

/**
 *
 */
CardJs.prototype.refreshCvc = function() {
  var numbersOnly = CardJs.numbersOnlyString($(this.cvcInput).val());
  var formattedNumber = CardJs.applyFormatMask(numbersOnly, this.creditCardNumberMask);
  $(this.cvcInput).val(formattedNumber);
};

// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

/**
 * Update the display to set the card type from the current number.
 *
 * @param number
 */
CardJs.prototype.setCardTypeIconFromNumber = function(number) {
  switch(CardJs.cardTypeFromNumber(number)) {
    case "Visa Electron":
    case "Visa":
      this.setCardTypeAsVisa();
      break;
    case "Mastercard":
      this.setCardTypeAsMasterCard();
      break;
    case "AMEX":
      this.setCardTypeAsAmericanExpress();
      break;
    case "Discover":
      this.setCardTypeAsDiscover();
      break;
    case "Diners - Carte Blanche":
    case "Diners":
      this.setCardTypeAsDiners();
      break;
    case "JCB":
      this.setCardTypeAsJcb();
      break;
    default:
      this.clearCardType();
  }
};

/**
 * Set the card number mask
 *
 * @param cardMask
 */
CardJs.prototype.setCardMask = function(cardMask) {
  this.creditCardNumberMask = cardMask;
  this.cardNumberInput.attr("maxlength", cardMask.length);
};

/**
 * Set the CVC with a mask of 3 digits
 */
CardJs.prototype.setCvc3 = function() {
  this.cvcInput.attr("maxlength", CardJs.CVC_MASK_3.length);
};

/**
 * Set the CVC with a mask of 4 digits
 */
CardJs.prototype.setCvc4 = function() {
  this.cvcInput.attr("maxlength", CardJs.CVC_MASK_4.length);
};

// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

/**
 * Reset the card type icon - show nothing
 */
CardJs.prototype.clearCardTypeIcon = function() {
  this.elem.find(".card-number-wrapper .card-type-icon").removeClass("show");
};

/**
 * Set the card type icon as - Visa
 */
CardJs.prototype.setCardTypeIconAsVisa = function() {
  this.elem.find(".card-number-wrapper .card-type-icon").attr("class", "card-type-icon show visa");
};

/**
 * Set the card type icon as - Master Card
 */
CardJs.prototype.setCardTypeIconAsMasterCard = function() {
  this.elem.find(".card-number-wrapper .card-type-icon").attr("class", "card-type-icon show master-card");
};

/**
 * Set the card type icon as - American Express (AMEX)
 */
CardJs.prototype.setCardTypeIconAsAmericanExpress = function() {
  this.elem.find(".card-number-wrapper .card-type-icon").attr("class", "card-type-icon show american-express");
};

/**
 * Set the card type icon as - Discover
 */
CardJs.prototype.setCardTypeIconAsDiscover = function() {
  this.elem.find(".card-number-wrapper .card-type-icon").attr("class", "card-type-icon show discover");
};

/**
 * Set the card type icon as - Diners
 */
CardJs.prototype.setCardTypeIconAsDiners = function() {
  this.elem.find(".card-number-wrapper .card-type-icon").attr("class", "card-type-icon show diners");
};

/**
 * Set the card type icon as - JCB
 */
CardJs.prototype.setCardTypeIconAsJcb = function() {
  this.elem.find(".card-number-wrapper .card-type-icon").attr("class", "card-type-icon show jcb");
};

// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

/**
 * Reset the card type
 */
CardJs.prototype.clearCardType = function() {
  this.clearCardTypeIcon();
  this.setCardMask(CardJs.CREDIT_CARD_NUMBER_DEFAULT_MASK);
  this.setCvc3();
};

/**
 * Set the card type as - Visa
 */
CardJs.prototype.setCardTypeAsVisa = function() {
  this.setCardTypeIconAsVisa();
  this.setCardMask(CardJs.CREDIT_CARD_NUMBER_VISA_MASK);
  this.setCvc3();
};

/**
 * Set the card type as - Master Card
 */
CardJs.prototype.setCardTypeAsMasterCard = function() {
  this.setCardTypeIconAsMasterCard();
  this.setCardMask(CardJs.CREDIT_CARD_NUMBER_MASTERCARD_MASK);
  this.setCvc3();
};

/**
 * Set the card type as - American Express (AMEX)
 */
CardJs.prototype.setCardTypeAsAmericanExpress = function() {
  this.setCardTypeIconAsAmericanExpress();
  this.setCardMask(CardJs.CREDIT_CARD_NUMBER_AMEX_MASK);
  this.setCvc4();
};

/**
 * Set the card type as - Discover
 */
CardJs.prototype.setCardTypeAsDiscover = function() {
  this.setCardTypeIconAsDiscover();
  this.setCardMask(CardJs.CREDIT_CARD_NUMBER_DISCOVER_MASK);
  this.setCvc3();
};

/**
 * Set the card type as - Diners
 */
CardJs.prototype.setCardTypeAsDiners = function() {
  this.setCardTypeIconAsDiners();
  this.setCardMask(CardJs.CREDIT_CARD_NUMBER_DINERS_MASK);
  this.setCvc3();
};

/**
 * Set the card type as - JCB
 */
CardJs.prototype.setCardTypeAsJcb = function() {
  this.setCardTypeIconAsJcb();
  this.setCardMask(CardJs.CREDIT_CARD_NUMBER_JCB_MASK);
  this.setCvc3();
};

// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

/**
 * Initialise the card number input
 */
CardJs.prototype.initCardNumberInput = function() {

  // Find or create the card number input element
  this.cardNumberInput = CardJs.detachOrCreateElement(this.elem, ".card-number", "<input class='card-number' />");

  // Ensure the card number element has a name
  if (!CardJs.elementHasAttribute(this.cardNumberInput, "name")) {
    this.cardNumberInput.attr("name", "card-number");
  }

  // Ensure the card number has a placeholder
  if (!CardJs.elementHasAttribute(this.cardNumberInput, "placeholder")) {
    this.cardNumberInput.attr("placeholder", CardJs.CREDIT_CARD_NUMBER_PLACEHOLDER);
  }

  this.cardNumberInput.attr("type", "tel");
  this.cardNumberInput.attr("maxlength", this.creditCardNumberMask.length);
  this.cardNumberInput.attr("x-autocompletetype", "cc-number");
  this.cardNumberInput.attr("autocompletetype", "cc-number");
  this.cardNumberInput.attr("autocorrect", "off");
  this.cardNumberInput.attr("spellcheck", "off");
  this.cardNumberInput.attr("autocapitalize", "off");

  // ---

  //
  // Events
  //
  var $this = this;
  this.cardNumberInput.keydown(function(e) {
    CardJs.handleCreditCardNumberKey(e, $this.creditCardNumberMask);
  });
  this.cardNumberInput.keyup(function() {
    $this.refreshCreditCardTypeIcon();
  });
  //this.cardNumberInput.change(CardJs.handleCreditCardNumberChange);
  this.cardNumberInput.on('paste', function() {
    setTimeout(function() {
      $this.refreshCreditCardNumberFormat();
      $this.refreshCreditCardTypeIcon();
    }, 1);
  });
};

/**
 * Initialise the name input
 */
CardJs.prototype.initNameInput = function() {

  // Enable name input if a field has been created
  this.captureName = this.elem.find(".name")[0] != null;

  // Find or create the name input element
  this.nameInput = CardJs.detachOrCreateElement(this.elem, ".name", "<input class='name' />");

  // Ensure the name element has a field name
  if (!CardJs.elementHasAttribute(this.nameInput, "name")) {
    this.nameInput.attr("name", "card-number");
  }

  // Ensure the name element has a placeholder
  if (!CardJs.elementHasAttribute(this.nameInput, "placeholder")) {
    this.nameInput.attr("placeholder", CardJs.NAME_PLACEHOLDER);
  }
};

/**
 * Initialise the expiry month input
 */
CardJs.prototype.initExpiryMonthInput = function() {
  this.expiryMonthInput = CardJs.detachOrCreateElement(this.elem, ".expiry-month", "<input class='expiry-month' />");
};

/**
 * Initialise the expiry year input
 */
CardJs.prototype.initExpiryYearInput = function() {
  this.expiryYearInput = CardJs.detachOrCreateElement(this.elem, ".expiry-year", "<input class='expiry-year' />");
};

/**
 * Initialise the card CVC input
 */
CardJs.prototype.initCvcInput = function() {

  // Find or create the CVC input element
  this.cvcInput = CardJs.detachOrCreateElement(this.elem, ".cvc", "<input class='cvc' />");

  // Ensure the CVC has a placeholder
  if (!CardJs.elementHasAttribute(this.cvcInput, "placeholder")) {
    this.cvcInput.attr("placeholder", CardJs.CVC_PLACEHOLDER);
  }

  this.cvcInput.attr("type", "tel");
  this.cvcInput.attr("maxlength", CardJs.CVC_MASK_3.length);
  this.cvcInput.attr("x-autocompletetype", "cc-csc");
  this.cvcInput.attr("autocompletetype", "cc-csc");
  this.cvcInput.attr("autocorrect", "off");
  this.cvcInput.attr("spellcheck", "off");
  this.cvcInput.attr("autocapitalize", "off");

  //
  // Events
  //
  var $this = this;
  this.cvcInput.keydown(CardJs.filterNumberOnlyKey);
  this.cvcInput.on('paste', function() {
    setTimeout(function() {
      $this.refreshCvc();
    }, 1);
  });
};

// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

CardJs.prototype.setupCardNumberInput = function() {
  if (this.stripe) { this.cardNumberInput.attr("data-stripe", "number"); }
  this.elem.append("<div class='card-number-wrapper'></div>");
  var wrapper = this.elem.find(".card-number-wrapper");
  wrapper.append(this.cardNumberInput);
  wrapper.append("<div class='card-type-icon'></div>");
  wrapper.append("<div class='icon'></div>");
  wrapper.find(".icon").append(CardJs.CREDIT_CARD_SVG);
};

CardJs.prototype.setupNameInput = function() {
  if (this.captureName) {
    this.elem.append("<div class='name-wrapper'></div>");
    var wrapper = this.elem.find(".name-wrapper");
    wrapper.append(this.nameInput);
    wrapper.append("<div class='icon'></div>");
    wrapper.find(".icon").append(CardJs.USER_SVG);
  }
};

CardJs.prototype.setupExpiryInput = function() {
  this.elem.append("<div class='expiry-container'><div class='expiry-wrapper'></div></div>");
  var wrapper = this.elem.find(".expiry-wrapper");

  var expiryInput;

  //
  // Use dropdowns
  //
  if(this.EXPIRY_USE_DROPDOWNS) {
    expiryInput = $("<div></div>");

    var expiryMonthNew = $(
      "<select>" +
        "<option value='any' selected='' hidden=''>MM</option>" +
        "<option value='1'>01</option>" +
        "<option value='2'>02</option>" +
        "<option value='3'>03</option>" +
        "<option value='4'>04</option>" +
        "<option value='5'>05</option>" +
        "<option value='6'>06</option>" +
        "<option value='7'>07</option>" +
        "<option value='8'>08</option>" +
        "<option value='9'>09</option>" +
        "<option value='10'>10</option>" +
        "<option value='11'>11</option>" +
        "<option value='12'>12</option>" +
        "</select>"
    );
    var expiryMonthOld = this.expiryMonthInput;
    CardJs.copyAllElementAttributes(expiryMonthOld, expiryMonthNew);
    this.expiryMonthInput.remove();
    this.expiryMonthInput = expiryMonthNew;

    var expiryYearNew = $("<select><option value='any' selected='' hidden=''>YY</option></select>");
    var currentYear = parseInt(((new Date().getFullYear()).toString()).substring(2, 4));
    for(var i = 0; i < CardJs.EXPIRY_NUMBER_OF_YEARS; i++) {
      expiryYearNew.append("<option value='" + currentYear + "'>" + currentYear + "</option>");
      currentYear = (currentYear + 1) % 100;
    }
    var expiryYearOld = this.expiryYearInput;
    CardJs.copyAllElementAttributes(expiryYearOld, expiryYearNew);
    this.expiryYearInput.remove();
    this.expiryYearInput = expiryYearNew;

    expiryInput.append(this.expiryMonthInput);
    expiryInput.append(this.expiryYearInput);

  }

  // ---

  //
  // Use single text field input for card expiry
  //
  else {
    expiryInput = $("<div></div>");

    // Ensure the expiry month is hidden
    if (this.expiryMonthInput.attr("type") != "hidden") {
      this.expiryMonthInput.attr("type", "hidden");
    }

    // Ensure the expiry year is hidden
    if (this.expiryYearInput.attr("type") != "hidden") {
      this.expiryYearInput.attr("type", "hidden");
    }

    // Add stripe data values to expiry inputs if stripe support enabled
    if (this.stripe) {
      this.expiryMonthInput.attr("data-stripe", "exp-month");
      this.expiryYearInput.attr("data-stripe", "exp-year");
    }

    // --- --- ---

    // Construct the single expiry input for both expiry month and year
    this.expiryMonthYearInput = CardJs.detachOrCreateElement(this.elem, ".expiry", "<input class='expiry' />");

    // Ensure the expiry input has a placeholder
    if (!CardJs.elementHasAttribute(this.expiryMonthYearInput, "placeholder")) {
      this.expiryMonthYearInput.attr("placeholder", CardJs.EXPIRY_PLACEHOLDER);
    }

    this.expiryMonthYearInput.attr("type", "tel");
    this.expiryMonthYearInput.attr("maxlength", CardJs.EXPIRY_MASK.length);
    this.expiryMonthYearInput.attr("x-autocompletetype", "cc-exp");
    this.expiryMonthYearInput.attr("autocompletetype", "cc-exp");
    this.expiryMonthYearInput.attr("autocorrect", "off");
    this.expiryMonthYearInput.attr("spellcheck", "off");
    this.expiryMonthYearInput.attr("autocapitalize", "off");

    // ---

    //
    // Events
    //
    var $this = this;
    this.expiryMonthYearInput.keydown(function(e) {
      CardJs.handleExpiryKey(e);

      var val = $this.expiryMonthYearInput.val();

      if(val.length == 1 && parseInt(val) > 1 && CardJs.keyIsNumber(e)) {
        $this.expiryMonthYearInput.val(CardJs.applyFormatMask("0" + val, CardJs.EXPIRY_MASK));
      }

      if(!$this.EXPIRY_USE_DROPDOWNS && $this.expiryMonthYearInput != null) {
        $this.expiryMonthInput.val($this.expiryMonth());

        $this.expiryYearInput.val(val.length == 7 ? val.substr(5,2) : null);
      }
    });

    this.expiryMonthYearInput.blur(function() {
      $this.refreshExpiryMonthValidation();
    });

    this.expiryMonthYearInput.on('paste', function() {
      setTimeout(function() {
        $this.refreshExpiryMonthYearInput();
      }, 1);
    });

    expiryInput.append(this.expiryMonthYearInput);
    expiryInput.append(this.expiryMonthInput);
    expiryInput.append(this.expiryYearInput);
  }

  wrapper.append(expiryInput);
  wrapper.append("<div class='icon'></div>");
  wrapper.find(".icon").append(CardJs.CALENDAR_SVG);
};

CardJs.prototype.setupCvcInput = function() {
  if(this.stripe) { this.cvcInput.attr("data-stripe", "cvc"); }
  this.elem.append("<div class='cvc-container'><div class='cvc-wrapper'></div></div>");
  var wrapper = this.elem.find(".cvc-wrapper");
  wrapper.append(this.cvcInput);
  wrapper.append("<div class='icon'></div>");
  wrapper.find(".icon").append(CardJs.LOCK_SVG);
  //wrapper.append("<div class='icon right popup'></div>");
  //wrapper.find(".icon.right").append(CardJs.INFORMATION_SVG);
};

CardJs.prototype.expiryMonth = function() {
  if(!this.EXPIRY_USE_DROPDOWNS && this.expiryMonthYearInput != null) {
    var val = this.expiryMonthYearInput.val();
    return val.length >= 2 ? parseInt(val.substr(0,2)) : null;
    //return (monthValue >= 1 && monthValue <= 12) ? monthValue : null;
  }
  return null;
};

/**
 * Refresh whether the expiry month is valid (update display to reflect)
 */
CardJs.prototype.refreshExpiryMonthValidation = function() {
  CardJs.isExpiryValid(this.getExpiryMonth(), this.getExpiryYear())
    ? this.setExpiryMonthAsValid() : this.setExpiryMonthAsInvalid();
};

/**
 * Update the display to highlight the expiry month as valid.
 */
CardJs.prototype.setExpiryMonthAsValid = function() {
  if(this.EXPIRY_USE_DROPDOWNS) {

  } else {
    this.expiryMonthYearInput.parent().removeClass("has-error");
  }
};

/**
 * Update the display to highlight the expiry month as invalid.
 */
CardJs.prototype.setExpiryMonthAsInvalid = function() {
  if (this.EXPIRY_USE_DROPDOWNS) {

  } else {
    this.expiryMonthYearInput.parent().addClass("has-error");
  }
};

// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

/**
 * Does the given element have an attribute with the given attribute name
 *
 * @param element
 * @param attributeName
 * @returns {boolean}
 */
CardJs.elementHasAttribute = function(element, attributeName) {
  var attr = $(element).attr(attributeName);
  return typeof attr !== typeof undefined && attr !== false;
};

/**
 * Detach an element if it exists, or create a new one if it doesn't.
 *
 * @param parentElement
 * @param selector
 * @param html
 * @returns {*}
 */
CardJs.detachOrCreateElement = function(parentElement, selector, html) {
  var element = parentElement.find(selector);
  if (element[0]) {
    element.detach();
  } else {
    element = $(html);
  }
  return element;
};

/**
 * Is the given month a valid month?
 *
 * @param expiryMonth
 * @returns {boolean}
 */
CardJs.isValidMonth = function(expiryMonth) {
  return (expiryMonth >= 1 && expiryMonth <= 12);
};

/**
 * Is the given card expiry (month and year) valid?
 *
 * @param month
 * @param year
 * @returns {boolean}
 */
CardJs.isExpiryValid = function(month, year) {
  var today = new Date();
  var currentMonth = (today.getMonth() + 1);
  var currentYear = "" + today.getFullYear();

  if (("" + year).length == 2) {
    year = currentYear.substring(0, 2) + "" + year;
  }

  currentMonth = parseInt(currentMonth);
  currentYear = parseInt(currentYear);
  month = parseInt(month);
  year = parseInt(year);

  return CardJs.isValidMonth(month)
    && ((year > currentYear) || (year == currentYear && month >= currentMonth));
};


const sqlite = require('nativescript-sqlite');
const ObservableArray = require("tns-core-modules/data/observable-array").ObservableArray;

//var Tracing = require('./tracing.js');
//Tracing(sqlite, {ignore: ["close", "resultType", "valueType", "_toStringArray", "_getResultEngine"], disableAddedFunction: true});

var dbname = 'name_db.sqlite';
var db = null;
var enc_db = null;
var page = null;

var data = new ObservableArray();

if (sqlite.HAS_COMMERCIAL) {
	console.log("Using Commercial");
	data.push({name:'Commercial Support', css:'one'});
} else {
	console.log("No Commercial Support");
}

if (sqlite.HAS_ENCRYPTION) {
	console.log("Using Encryption");
	//dbname = 'encrypted.sqlite';
	data.push({name:'Encryption Support', css:'one'});
} else {
	console.log("No Encryption");
}
data.push({name: 'Loading...', css: 'one'});

if (sqlite.HAS_SYNC) {
    console.log("Using Sync");
    data.push({name: 'Sync Support',css: 'one'});
} else {
    console.log("No Sync");
}

exports.pageLoaded = function(args) {
	page = args.object;
	page.bindingContext = {names: data};

	if (!sqlite.exists(dbname)) {
		sqlite.copyDatabase(dbname);
	}
	new sqlite(dbname, {key: 'testing', multithreading: !!sqlite.HAS_COMMERCIAL, migrate: true}, function(err, dbConnection) {
		if (err) {
			console.log(err, err.stack);
		}
		db = dbConnection;
		db.resultType(sqlite.RESULTSASOBJECT);

		db.version().then(function (results) {
			console.log("User Version: ", results, typeof results, Number.isNumber(results)); //, String.isString(results));
		});

		if (sqlite.HAS_ENCRYPTION) {
			db.get("PRAGMA cipher_version;").then(function(results) {
				console.log("Cipher version", results['cipher_version']);
			});
		}

		reloadData();
	}).catch(function(val) {
		console.log("sqlite error", val);
	});
};

exports.addNewName = function() {
	const entry = page.getViewById('entry');
	const name = entry.text;
	if (name.length > 0) {
		if (name.toLowerCase() === "test" || name.toLowerCase() === "runtest" || name.toLowerCase() === "tests" || name.toLowerCase() === "runtests" || name.indexOf("Test") === 0) {
			runTests();
			return;
		}

		if (name.toLowerCase() === "sync") {
		    db.enableTracking("names", {'syncTime': 10}).then((res) => {
		        console.log("Result", res);
            }).catch((err) => {
                console.log("Error",err, err.stack);
            });
		    return;
        }

		if (name.toLowerCase() === "fsync") {
			db.enableTracking("names", {force: true, syncTime: 10}).then((res) => {
				console.log("Result", res);
			}).catch((err) => {
				console.log("Error",err, err.stack);
			});
			return;
		}

		if (name.toLowerCase() === "csync") {
			db.execSQL("update __mt_sync_tracking set completed=1");
			return;
		}

		if (name.toLowerCase() === "dsync") {
			db.execSQL("delete from __mt_sync_tracking");
			return;
		}

		if (name.toLowerCase() === 'tsync') {
			db.all("select * from __mt_sync_tracking").then((res) => {
				console.log("Results:", res);
			});
			return;
		}

		db.execSQL("insert into names (name) values (?)", name);
		reloadData();
	}
	entry.text = '';
};

exports.openMT = function() {
   const utils = require('tns-core-modules/utils/utils');
   utils.openUrl("https://www.master-technology.com");
};

function reloadData() {
	db.resultType(sqlite.RESULTSASOBJECT);
	db.valueType(sqlite.VALUESARENATIVE);

	db.all('select name from names', function (err, loadedData) {
		data.length = 0;
		if (err) {
			console.log(err);
		} else {
			for (var i=0;i<loadedData.length;i++) {
				if (i % 2 === 0) {
					loadedData[i].css = "one";
				} else {
					loadedData[i].css = "two";
				}
				data.push(loadedData[i]);
			}
		}
	});
}

function setupTests(callback) {
	data.push({name: 'Creating tables and data...', css: 'one'});
	db.execSQL('drop table if exists tests;', function(err) {
		if (err) { console.log("!---- Drop Err", err); }
		db.execSQL('create table tests (`int_field` integer, `num_field` numeric, `real_field` real, `text_field` text)', function(err) {
			if (err) {
				data.push({name: 'Failed to create tables and data...', css: 'one'});
				console.log("!---- Create Table err", err);
				return;
			}
			db.execSQL('insert into tests (int_field, num_field, real_field, text_field) values (1,1.2,2.4,"Text1")', function (err) {
				if (err) {
					data.push({name: 'Failed to create tables and data...', css: 'one'});
					console.log("!---- Insert err", err);
					return;
				}
				db.execSQL('insert into tests (int_field, num_field, real_field, text_field) values (2,4.8,5.6,"Text2")').then(() => {
					callback();
				}).catch((err) => {
					if (err) {
						data.push({name: 'Failed to insert data into tests', css: 'one'});
						console.log("!---- Insert err2:", err);
						return;
					}
					callback(err);
				});
			});
		});
	});
}

function checkRowOfData(inData, validData) {
	if (Array.isArray(inData)) {
		for (var i = 0; i < inData.length; i++) {
			if (typeof inData[i] === "number") {
				if (inData[i] !== validData[i]) {
					if (inData[i]-0.1 > validData[i] || inData[i]+0.1 < validData[i]) {
						return ({status: false, field: i});
					}
				}
			} else {
				if (inData[i] !== validData[i]) {
					return ({status: false, field: i});
				}
			}
		}
	} else {
		for (var key in inData) {
			if (inData.hasOwnProperty(key)) {
				if (typeof inData[key] === "number") {
					if (inData[key] !== validData[key]) {
						if (inData[key]-0.1 > validData[key] || inData[key]+0.1 < validData[key]) {
							return ({status: false, field: key});
						}
					}
				} else {
					if (inData[key] !== validData[key]) {
						return ({status: false, field: key});
					}
				}
			}
		}
	}
	return {status: true};
}

function runATest(options, callback) {

	//console.log("!--------------  Starting Test", options.name);

	//data.push({name: "Starting test"+options.name});
	var checkResults = function(err, inData) {
		//console.log("!--------------  Checking Results", options.name, "Error: ", err, "Data:", inData);
		var passed = true;
		if (err) {
			console.log("!------------ Error", err.toString());
			data.push({name: options.name + " test failed with: ", css: 'one'});
			data.push({name: "  " + err.toString(), css:'one'});
			return callback(false);
		}
		if (!inData || inData.length !== options.results.length) {

			console.dir(inData);
			console.log("!----------- No Data");
			data.push({name: options.name + " test failed with different results length", css: 'one'});
			return callback(false);
		}
		if (inData.length === 0) {
			console.log("!-------- No Data Returned");
			return callback(passed);
		}
		//console.log("!------------ Data Returned", inData.length, inData);
		for (var i=0;i<inData.length;i++) {
			var result = checkRowOfData(inData[i], options.results[i]);
			if (!result.status) {
				passed = false;
				data.push({name: options.name + " test failed on row: "+i+", field: "+result.field, css: 'one'});
				console.log("$$$$$ Failure:", inData[i], options.results[i], typeof inData[i][result.field], typeof options.results[i][result.field]);
				break;
			}
		}
		callback(passed);
	};

	var checkRow = 0;
	var checksPassed = true;
	var checkEachResults = function(err, inData) {
		if (!checksPassed) return;
		if (err) {
			data.push({name: options.name + " test failed with "+err.toString(), css: 'one'});
			console.log(options.name + " test failed with ",err.toString());
			checksPassed = false;
			return;
		}
		var result = checkRowOfData(inData, options.results[checkRow]);
		if (!result.status) {
			checksPassed = false;
			data.push({name: options.name + " test failed on row: "+checkRow+", field: "+result.field, css: 'one'});
			console.log("$$$$ Failure: ", inData, options.results[checkRow], typeof inData[result.field], typeof options.results[checkRow][result.field]);
		}
		checkRow++;
	};

	const checkFinalResults = function() {
		callback(checksPassed);
	};

	const promiseResults = function(res) {
		checkResults(null, res);
	};
	const promiseFailed = function(err) {
		checkResults(err);
	};

	if (options.values) {
		switch (options.use) {
			case 0:
				db.get(options.sql, options.values, checkResults);
				break;
			case 1:
				db.all(options.sql, options.values, checkResults);
				break;
			case 2:
				db.each(options.sql, options.values, checkEachResults, checkFinalResults);
				break;

			case 3:
				db.get(options.sql, options.values).then(promiseResults).catch(promiseFailed);
				break;
			case 4:
				db.all(options.sql, options.values).then(promiseResults).catch(promiseFailed);
				break;
			case 5:
				db.each(options.sql, options.values, checkEachResults).then(checkFinalResults).catch(promiseFailed);
				break;

			case 6:
				db.execSQL(options.sql, options.params, checkResults);
				break;

			case 7:
				db.execSQL(options.sql, options.params).then(promiseResults).catch(promiseFailed);
				break;

			default:
				callback(false);
		}
	} else {
		switch (options.use) {
			case 0:
				db.get(options.sql, checkResults);
				break;
			case 1:
				db.all(options.sql, checkResults);
				break;
			case 2:
				db.each(options.sql, checkEachResults, checkFinalResults);
				break;

			case 3:
				db.get(options.sql).then(promiseResults).catch(promiseFailed);
				break;
			case 4:
				db.all(options.sql).then(promiseResults).catch(promiseFailed);
				break;
			case 5:
				db.each(options.sql, checkEachResults).then(checkFinalResults).catch(promiseFailed);
				break;

			case 6:
				db.execSQL(options.sql, checkResults);
				break;

			case 7:
				db.execSQL(options.sql).then(promiseResults).catch(promiseFailed);
				break;

			default:
				callback(false);
		}
	}
}

function runTestGroup(tests, callback) {
	let runningTest = -1;
	const runTest = function(status) {
		if (!status) {
			return callback(false);
		} else if (runningTest > -1) {
			data.push({name: "Passed: " + tests[runningTest].name, 'css':'two'});
		}
		runningTest++;
		if (runningTest >= tests.length) {
			return callback(status);
		}
		runATest(tests[runningTest], runTest);
	};

	data.push({name: "-----------------------------", css: 'two'});
	runTest(true);
}

function runNativeArrayTest(callback) {
	console.log("!--------------  Starting RNA Test");
	db.resultType(sqlite.RESULTSASARRAY);
	db.valueType(sqlite.VALUESARENATIVE);

	const tests = [
		// Callback
		{name: 'NativeArray Check', sql: 'select count(*) from tests', results: [2], use: 0},
		{name: 'NativeArray Get', sql: 'select * from tests where int_field=?', values: [2], results: [2,4.8,5.6,'Text2'], use: 0},
		{name: 'NativeArray All',    sql: 'select * from tests order by int_field', results: [[1,1.2,2.4,"Text1"],[2,4.8,5.6,'Text2']], use: 1},
		{name: 'NativeArray Each', sql: 'select * from tests order by int_field', results: [[1,1.2,2.4,"Text1"],[2,4.8,5.6,'Text2']], use: 2},

		// Promise
		{name: 'NativeArray Promise Check', sql: 'select count(*) from tests', results: [2], use: 3},
		{name: 'NativeArray Promise Get', sql: 'select * from tests where int_field=?', values: [2], results: [2,4.8,5.6,'Text2'], use: 3},
		{name: 'NativeArray Promise All',    sql: 'select * from tests order by int_field', results: [[1,1.2,2.4,"Text1"],[2,4.8,5.6,'Text2']], use: 4},
		{name: 'NativeArray Promise Each', sql: 'select * from tests order by int_field', results: [[1,1.2,2.4,"Text1"],[2,4.8,5.6,'Text2']], use: 5}

	];
	runTestGroup(tests, callback);
}

function runStringArrayTest(callback) {
	console.log("!--------------  Starting RSA Test");
	db.resultType(sqlite.RESULTSASARRAY);
	db.valueType(sqlite.VALUESARESTRINGS);
	const tests = [
		// Callback Version
		{name: 'StringArray Get', sql: 'select * from tests where int_field=?', values: [2], results: ["2","4.8","5.6",'Text2'], use: 0},
		{name: 'StringArray All', sql: 'select * from tests order by int_field', results: [["1","1.2","2.4","Text1"],["2","4.8","5.6",'Text2']], use: 1},
		{name: 'StringArray Each', sql: 'select * from tests order by int_field', results: [["1","1.2","2.4","Text1"],["2","4.8","5.6",'Text2']], use: 2},

		// Promise Version
		{name: 'StringArray Promise Get', sql: 'select * from tests where int_field=?', values: [2], results: ["2","4.8","5.6",'Text2'], use: 3},
		{name: 'StringArray Promise All', sql: 'select * from tests order by int_field', results: [["1","1.2","2.4","Text1"],["2","4.8","5.6",'Text2']], use: 4},
		{name: 'StringArray Promise Each', sql: 'select * from tests order by int_field', results: [["1","1.2","2.4","Text1"],["2","4.8","5.6",'Text2']], use: 5}
	];
	runTestGroup(tests, callback);
}

function runNativeObjectTest(callback) {
	console.log("!--------------  Starting RNO Test");
	db.resultType(sqlite.RESULTSASOBJECT);
	db.valueType(sqlite.VALUESARENATIVE);

	const tests = [
		// Callback
		{name: 'NativeObject Get', sql: 'select * from tests where int_field=?', values: [2], results: {int_field: 2, num_field: 4.8, real_field: 5.6, text_field: 'Text2'}, use: 0},
		{name: 'NativeObject All', sql: 'select * from tests order by int_field', results: [{int_field: 1, num_field: 1.2, real_field: 2.4, text_field: 'Text1'},{int_field: 2, num_field: 4.8, real_field: 5.6, text_field: 'Text2'}], use: 1},
		{name: 'NativeObject Each', sql: 'select * from tests order by int_field', results: [{int_field: 1, num_field: 1.2, real_field: 2.4, text_field: 'Text1'},{int_field: 2, num_field: 4.8, real_field: 5.6, text_field: 'Text2'}], use: 2},

		// Promise
		{name: 'NativeObject Promise Get', sql: 'select * from tests where int_field=?', values: [2], results: {int_field: 2, num_field: 4.8, real_field: 5.6, text_field: 'Text2'}, use: 3},
		{name: 'NativeObject Promise All', sql: 'select * from tests order by int_field', results: [{int_field: 1, num_field: 1.2, real_field: 2.4, text_field: 'Text1'},{int_field: 2, num_field: 4.8, real_field: 5.6, text_field: 'Text2'}], use: 4},
		{name: 'NativeObject Promise Each', sql: 'select * from tests order by int_field', results: [{int_field: 1, num_field: 1.2, real_field: 2.4, text_field: 'Text1'},{int_field: 2, num_field: 4.8, real_field: 5.6, text_field: 'Text2'}], use: 5}

	];
	runTestGroup(tests, callback);
}

function runStringObjectTest(callback) {
	console.log("!--------------  Starting RSO Test");
	db.resultType(sqlite.RESULTSASOBJECT);
	db.valueType(sqlite.VALUESARENATIVE);

	const tests = [
		// Callback
		{name: 'StringObject Get', sql: 'select * from tests where int_field=?', values: [2], results: {int_field: "2", num_field: "4.8", real_field: "5.6", text_field: 'Text2'}, use: 0},
		{name: 'StringObject All', sql: 'select * from tests order by int_field', results: [{int_field: "1", num_field: "1.2", real_field: "2.4", text_field: 'Text1'},{int_field: "2", num_field: "4.8", real_field: "5.6", text_field: 'Text2'}], use: 1},
		{name: 'StringObject Each', sql: 'select * from tests order by int_field', results: [{int_field: "1", num_field: "1.2", real_field: "2.4", text_field: 'Text1'},{int_field: "2", num_field: "4.8", real_field: "5.6", text_field: 'Text2'}], use: 2},

		// Promise
		{name: 'StringObject Promise Get', sql: 'select * from tests where int_field=?', values: [2], results: {int_field: "2", num_field: "4.8", real_field: "5.6", text_field: 'Text2'}, use: 3},
		{name: 'StringObject Promise All', sql: 'select * from tests order by int_field', results: [{int_field: "1", num_field: "1.2", real_field: "2.4", text_field: 'Text1'},{int_field: "2", num_field: "4.8", real_field: "5.6", text_field: 'Text2'}], use: 4},
		{name: 'StringObject Promise Each', sql: 'select * from tests order by int_field', results: [{int_field: "1", num_field: "1.2", real_field: "2.4", text_field: 'Text1'},{int_field: "2", num_field: "4.8", real_field: "5.6", text_field: 'Text2'}], use: 5}

	];
	runTestGroup(tests, callback);
}

function setupPreparedTests(callback) {
	if (!sqlite.HAS_COMMERCIAL) {
		callback();
		return;
	}

    console.log("!--------- Creating Prepared Tests Data");
    db.execSQL(['drop table if exists preparetests;','create table preparetests (`int_field` integer, `num_field` numeric, `real_field` real, `text_field` text)'], function (err) {
        if (err) {
            data.push({name: 'Failed to create tables and data...', css: 'one'});
            console.log("!---- Create Table err", err);
            return;
        }
        callback();
    });

    /*
	db.execSQL('drop table if exists preparetests;', function (err) {
		if (err) {
			console.log("!---- Drop Err", err);
		}
		db.execSQL('create table preparetests (`int_field` integer, `num_field` numeric, `real_field` real, `text_field` text)', function (err) {
			if (err) {
				data.push({name: 'Failed to create tables and data...', css: 'one'});
				console.log("!---- Create Table err", err);
				return;
			}
			callback();
		});
	}); */
}

function runPreparedTests(callback) {
	if (!sqlite.HAS_COMMERCIAL) {
		callback();
		return;
	}
	db.resultType(sqlite.RESULTSASARRAY);
	db.valueType(sqlite.VALUESARENATIVE);

	setupPreparedTests(function() {
		createPreparedData(true, function () {

			let tests = [{
				name: 'Verify Rollback Check',
				sql: 'select count(*) from preparetests',
				results: [0],
				use: 0
			}];
			runTestGroup(tests, function () {
				createPreparedData(false, function () {

					tests = [{
						name: 'Verify Commit Check',
						sql: 'select count(*) from preparetests',
						results: [3],
						use: 0
					},
						{
							name: 'Commit/Prepare All', sql: 'select * from preparetests order by int_field', results: [
								[1, 1.2, 2.4, 'Text1'],
								[2, 2.4, 3.6, 'Text2'],
								[3, 3.6, 4.8, 'Text3']
							], use: 1
						}];
					runTestGroup(tests, callback);
				});
			});
		});
	});
}

function createPreparedData(rollback, callback) {
	if (!sqlite.HAS_COMMERCIAL) {
		callback();
		return;
	}
	try {
		console.log("!------------- Create Prepared Tests");
		var prepared = db.prepare("insert into preparetests (int_field, num_field, real_field, text_field) values (?,?,?,?);");
	} catch(err) {
		console.log("Error creating prepare data", err);
	}
	db.begin();
	prepared.execute([1,1.2,2.4,"Text1"], function(err) {
		if (err) {
			data.push({name: 'Failed to insert data...', 'css': 'one'});
			console.log("!---- Insert err", err, err.stack);
			return;
		}
		prepared.execute([[2,2.4,3.6,"Text2"], [3,3.6,4.8,"Text3"]], function(err2) {
			if (err2) {
				data.push({name: 'Failed to create tables and data...', css: 'one'});
				console.log("!---- Insert err", err, err && err.stack);
				return;
			}
			if (rollback) {
				db.rollback();
			} else {
				db.commit();
			}
			prepared.finished();
			callback();
		});
	});
}

function runTests() {
	data.length = 0;
	data.push({name: 'Running SQLite tests...', css: 'one'});
	setupTests(function() {
		setTimeout( function() {
			data.push({name: 'Created tables & data...', css: 'one'});
			runNativeArrayTest(function () {
				runNativeObjectTest(function () {
					runStringArrayTest(function () {
						runStringObjectTest(function () {
							runPreparedTests(function () {
								data.push({name: "-----------------------------", css: 'two'});
								data.push({name: 'Tests completed...', css: 'two'});
							});
						});
					});
				});
			});
		},10);
	});
}


/**************************************************************************************
 * (c) 2015-2019, Master Technology
 * Licensed under the MIT license or contact me for a support, changes, enhancements,
 * and/or if you require a commercial licensing
 *
 * Any questions please feel free to put a issue up on github
 * Nathan@master-technology.com                           http://nativescript.tools
 * Version 2.3.2 - Android
 *************************************************************************************/

/* global global, require, module */

"use strict";
const appModule = require("application");
const fsModule  = require("file-system");

/*jshint undef: true */
/*global java, android, Promise */

// Needed for Creating Database - Android Specific flag
//var CREATEIFNEEDED = 0x10000000;

// Used to track any plugin Init
let _DatabasePluginInits = [];

/***
 * Parses a Row of data into a JS Array (as Native)
 * @param cursor {Object}
 * @returns {Array}
 */
function DBGetRowArrayNative(cursor) {
    //noinspection JSUnresolvedFunction
    let count = cursor.getColumnCount();
    let results = [];
    for (let i=0;i<count;i++) {
        const type = cursor.getType(i);
        switch (type) {
            case 0: // NULL
                results.push(null);
                break;

            case 1: // Integer
                //noinspection JSUnresolvedFunction
                results.push(cursor.getLong(i));
                break;

            case 2: // Float
                //noinspection JSUnresolvedFunction
                results.push(cursor.getFloat(i));
                break;

            case 3: // String
                //noinspection JSUnresolvedFunction
                results.push(cursor.getString(i));
                break;

            case 4: // Blob
                // noinspection JSCheckFunctionSignatures
                results.push(cursor.getBlob(i));
                break;

            default:
                throw new Error('SQLITE - Unknown Field Type ' + type);
        }
    }
    return results;
}

/***
 * Parses a Row of data into a JS Array (as String)
 * @param cursor
 * @returns {Array}
 */
function DBGetRowArrayString(cursor) {
    //noinspection JSUnresolvedFunction
    let count = cursor.getColumnCount();
    let results = [];
    for (let i=0;i<count;i++) {
        const type = cursor.getType(i);
        switch (type) {
            case 0: // NULL
                results.push(null);
                break;

            case 1: // Integer
                //noinspection JSUnresolvedFunction
                results.push(cursor.getString(i));
                break;

            case 2: // Float
                //noinspection JSUnresolvedFunction
                results.push(cursor.getString(i));
                break;

            case 3: // String
                //noinspection JSUnresolvedFunction
                results.push(cursor.getString(i));
                break;

            case 4: // Blob
                // noinspection JSCheckFunctionSignatures
                results.push(cursor.getBlob(i));
                break;

            default:
                throw new Error('SQLITE - Unknown Field Type ' + type);
        }
    }
    return results;
}

/***
 * Parses a Row of data into a JS Object (as Native)
 * @param cursor
 * @returns {{}}
 */
function DBGetRowObjectNative(cursor) {
    //noinspection JSUnresolvedFunction
    const count = cursor.getColumnCount();
    let results = {};
    for (let i=0;i<count;i++) {
        const type = cursor.getType(i);
        //noinspection JSUnresolvedFunction
        const name = cursor.getColumnName(i);
        switch (type) {
            case 0: // NULL
                results[name] = null;
                break;

            case 1: // Integer
                //noinspection JSUnresolvedFunction
                results[name] = cursor.getLong(i);
                break;

            case 2: // Float
                //noinspection JSUnresolvedFunction
                results[name] = cursor.getFloat(i);
                break;

            case 3: // String
                //noinspection JSUnresolvedFunction
                results[name] = cursor.getString(i);
                break;

            case 4: // Blob
                // noinspection JSCheckFunctionSignatures
                results[name] = cursor.getBlob(i);
                break;

            default:
                throw new Error('SQLITE - Unknown Field Type '+ type);
        }
    }
    return results;
}

/***
 * Parses a Row of data into a JS Object (as String)
 * @param cursor
 * @returns {{}}
 */
function DBGetRowObjectString(cursor) {
    //noinspection JSUnresolvedFunction
    const count = cursor.getColumnCount();
    let results = {};
    for (let i=0;i<count;i++) {
        const type = cursor.getType(i);
        //noinspection JSUnresolvedFunction
        const name = cursor.getColumnName(i);
        switch (type) {
            case 0: // NULL
                results[name] = null;
                break;

            case 1: // Integer
                //noinspection JSUnresolvedFunction
                results[name] = cursor.getString(i);
                break;

            case 2: // Float
                //noinspection JSUnresolvedFunction
                results[name] = cursor.getString(i);
                break;

            case 3: // String
                //noinspection JSUnresolvedFunction
                results[name] = cursor.getString(i);
                break;

            case 4: // Blob
                // noinspection JSCheckFunctionSignatures
                results[name] = cursor.getBlob(i);
                break;

            default:
                throw new Error('SQLITE - Unknown Field Type '+ type);
        }
    }
    return results;
}

// Default Resultset engine
let DBGetRowResults = DBGetRowArrayNative;

function setResultValueTypeEngine(resultType, valueType) {
    if (resultType === Database.RESULTSASOBJECT) {
        if (valueType === Database.VALUESARENATIVE) {
            DBGetRowResults = DBGetRowObjectNative;
        } else {
            DBGetRowResults = DBGetRowObjectString;
        }
    } else { // RESULTSASARRAY
        if (valueType === Database.VALUESARENATIVE) {
            DBGetRowResults = DBGetRowArrayNative;
        } else {
            DBGetRowResults = DBGetRowArrayString;
        }
    }
}

/***
 * Database Constructor
 * @param dbname - Database Name
 * @param callback - Callback when Done
 * @param options
 * @returns {Promise} object
 * @constructor
 */
function Database(dbname, options, callback) {
	if (!this instanceof Database) { // jshint ignore:line
		//noinspection JSValidateTypes
		return new Database(dbname, options, callback);
	}
	this._isOpen = false;
	this._resultType = Database.RESULTSASARRAY;
	this._valuesType = Database.VALUESARENATIVE;

	if (typeof options === 'function') {
		callback = options;
		//noinspection JSUnusedAssignment
		options = {};
	} else {
		//noinspection JSUnusedAssignment
		options = options || {};
	}

    //noinspection JSUnresolvedVariable
    if (options && options.multithreading && typeof global.Worker === 'function') {
	       // We don't want this passed into the worker; to try and start another worker (which would fail).
	        delete options.multithreading;
	        if (!Database.HAS_COMMERCIAL) {
	            throw new Error("Commercial only feature; see http://nativescript.tools/product/10");
            }
            return new Database._multiSQL(dbname, options, callback);
    }

	// Check to see if it has a path, or if it is a relative dbname
	// dbname = "" - Temporary Database
	// dbname = ":memory:" = memory database
	if (dbname !== "" && dbname !== ":memory:") {
		//var pkgName = appModule.android.context.getPackageName();
		//noinspection JSUnresolvedFunction
		dbname = _getContext().getDatabasePath(dbname).getAbsolutePath().toString();
		let path = dbname.substr(0, dbname.lastIndexOf('/') + 1);

		// Create "databases" folder if it is missing.  This causes issues on Emulators if it is missing
		// So we create it if it is missing

		try {
            //noinspection JSUnresolvedFunction,JSUnresolvedVariable
            let javaFile = new java.io.File(path);
			if (!javaFile.exists()) {
				//noinspection JSUnresolvedFunction
				javaFile.mkdirs();
				//noinspection JSUnresolvedFunction
				javaFile.setReadable(true);
				//noinspection JSUnresolvedFunction
				javaFile.setWritable(true);
			}
		}
		catch (err) {
			console.info("SQLITE.CONSTRUCTOR - Creating DB Folder Error", err);
		}
	}
	const self = this;

	return new Promise(function (resolve, reject) {
		try {
			let flags = 0;
			if (typeof options.androidFlags !== 'undefined') {
				flags = options.androidFlags;
			}
			self._db = self._openDatabase(dbname, flags, options, _getContext());
		} catch (err) {
			console.error("SQLITE.CONSTRUCTOR -  Open DB Error", err);
			if (callback) {
				callback(err, null);
			}
			reject(err);
			return;
		}

		self._isOpen = true;

		let doneCnt = _DatabasePluginInits.length, doneHandled = 0;
		const done = function (err) {
			if (err) {
				doneHandled = doneCnt;  // We don't want any more triggers after this
				if (callback) {
					callback(err, null);
				}
				reject(err);
				return;
			}
			doneHandled++;
			if (doneHandled === doneCnt) {
				if (callback) {
					callback(null, self);
				}
				resolve(self);
			}
		};

		if (doneCnt) {
			try {
				for (let i = 0; i < doneCnt; i++) {
					_DatabasePluginInits[i].call(self, options, done);
				}
			}
			catch (err) {
				done(err);
			}
		} else {
			if (callback) {
				callback(null, self);
			}
			resolve(self);
		}

	});
}

/**
 * Function to handle opening Database
 * @param dbName
 * @param flags
 * @private
 */
Database.prototype._openDatabase = function(dbName, flags) {
	if (dbName === ":memory:") {
		//noinspection JSUnresolvedVariable
		return android.database.sqlite.SQLiteDatabase.create(flags);
	} else {
		//noinspection JSUnresolvedVariable,JSUnresolvedFunction
		return android.database.sqlite.SQLiteDatabase.openDatabase(dbName, null, flags | 0x10000000);
	}
};

/***
 * Constant that this structure is a sqlite structure
 * @type {boolean}
 */
Database.prototype._isSqlite = true;

/***
 * This gets or sets the database version
 * @param valueOrCallback to set or callback(err, version)
 * @returns Promise
 */
Database.prototype.version = function(valueOrCallback) {
    if (typeof valueOrCallback === 'function') {
        return this.get('PRAGMA user_version', function (err, data) {
            valueOrCallback(err, data && parseInt(data[0],10));
        }, Database.RESULTSASARRAY);
    } else if (!isNaN(valueOrCallback+0)) {
        return this.execSQL('PRAGMA user_version='+(valueOrCallback+0).toString());
    } else {
        return this.get('PRAGMA user_version', undefined, undefined, Database.RESULTSASARRAY);
    }
};

/***
 * Is the database currently open
 * @returns {boolean} - true if the db is open
 */
Database.prototype.isOpen = function() {
    return this._isOpen;
};

/***
 * Gets/Sets whether you get Arrays or Objects for the row values
 * @param value - Database.RESULTSASARRAY or Database.RESULTSASOBJECT
 * @returns {number} - Database.RESULTSASARRAY or Database.RESULTSASOBJECT
 */
Database.prototype.resultType = function(value) {
    if (value === Database.RESULTSASARRAY) {
        this._resultType = Database.RESULTSASARRAY;
        setResultValueTypeEngine(this._resultType, this._valuesType);

    } else if (value === Database.RESULTSASOBJECT) {
        this._resultType = Database.RESULTSASOBJECT;
        setResultValueTypeEngine(this._resultType, this._valuesType);
    }
    return this._resultType;
};

/***
 * Gets/Sets whether you get Native or Strings for the row values
 * @param value - Database.VALUESARENATIVE or Database.VALUESARESTRINGS
 * @returns {number} - Database.VALUESARENATIVE or Database.VALUESARESTRINGS
 */
Database.prototype.valueType = function(value) {
    if (value === Database.VALUESARENATIVE) {
        this._valuesType = Database.VALUESARENATIVE;
        setResultValueTypeEngine(this._resultType, this._valuesType);

    } else if (value === Database.VALUESARESTRINGS) {
        this._valuesType = Database.VALUESARESTRINGS;
        setResultValueTypeEngine(this._resultType, this._valuesType);
    }
    return this._valuesType;
};

// noinspection JSUnusedLocalSymbols
/**
 * Dummy transaction function for public version
 * @param callback
 * @returns {Promise}
 */
Database.prototype.begin = function(callback) {
  throw new Error("Transactions are a Commercial version feature.");
};

// noinspection JSUnusedLocalSymbols
/**
 * Dummy prepare function for public version
 * @param sql
 * @returns {*}
 */
Database.prototype.prepare = function(sql) {
	throw new Error("Prepared statements are a Commercial version feature.");
};

/***
 * Closes this database, any queries after this will fail with an error
 * @param callback
 */
Database.prototype.close = function(callback) {

    const self = this;
    return new Promise(function(resolve, reject) {
        if (!self._isOpen) {
            if (callback) {
                callback('SQLITE.CLOSE - Database is already closed');
            }
            reject('SQLITE.CLOSE - Database is already closed');
            return;
        }

        self._db.close();
        self._isOpen = false;
        if (callback) {
            callback(null, null);
        }
        resolve();
    });
};

/***
 * Exec SQL
 * @param sql - sql to use
 * @param params - optional array of parameters
 * @param callback - (err, result) - can be last_row_id for insert, and rows affected for update/delete
 * @returns Promise
 */
Database.prototype.execSQL = function(sql, params, callback) {
    if (typeof params === 'function') {
        callback = params;
        params = undefined;
    }

    const self = this;
    return new Promise(function(resolve, reject) {
        let hasCallback = true;
        if (typeof callback !== 'function') {
            callback = reject;
            hasCallback = false;
        }

        if (!self._isOpen) {
            callback("SQLITE.EXECSQL - Database is not open");
            return;
        }

        // Need to see if we have to run any status queries afterwords
        let flags = 0;
        let test = sql.trim().substr(0, 7).toLowerCase();
        if (test === 'insert ') {
            flags = 1;
        } else if (test === 'update ' || test === 'delete ') {
            flags = 2;
        }

        try {
            if (params !== undefined) {
                self._db.execSQL(sql, self._toStringArray(params));
            } else {
                self._db.execSQL(sql);
            }
        } catch (Err) {
            callback(Err, null);
            return;
        }

        switch (flags) {
            case 0:
                if (hasCallback) {
                    callback(null, null);
                }
                resolve(null);
                break;
            case 1:
                // noinspection JSIgnoredPromiseFromCall
                self.get('select last_insert_rowid()', function (err, data) {
                    if (hasCallback) {
                        callback(err, data && data[0]);
                    }
                    if (err) {
                        reject(err);
                    } else {
                        resolve(data && data[0]);
                    }
                }, Database.RESULTSASARRAY | Database.VALUESARENATIVE);
                break;
            case 2:
                // noinspection JSIgnoredPromiseFromCall
                self.get('select changes()', function (err, data) {
                    if (hasCallback) {
                        callback(err, data && data[0]);
                    }
                    if (err) {
                        reject(err);
                    } else {
                        resolve(data && data[0]);
                    }
                }, Database.RESULTSASARRAY | Database.VALUESARENATIVE);
                break;
            default:
                resolve();
        }

    });
};

/***
 * Get the first record result set
 * @param sql - sql to run
 * @param params - optional
 * @param callback - callback (error, results)
 * @param mode - allows you to manually override the results set to be a array or object
 * @returns Promise
 */
Database.prototype.get = function(sql, params, callback, mode) {
    if (typeof params === 'function') {
        mode = callback;
        callback = params;
        params = undefined;
    }

    const self = this;
    return new Promise(function(resolve, reject) {
        let hasCallback = true;
        if (typeof callback !== 'function') {
            callback = reject;
            hasCallback = false;
        }

        if (!self._isOpen) {
            callback("SQLITE.GET - Database is not open", null);
            return;
        }

        let cursor;
        try {
            if (params !== undefined) {
                //noinspection JSUnresolvedFunction
                cursor = self._db.rawQuery(sql, self._toStringArray(params));
            } else {
                //noinspection JSUnresolvedFunction
                cursor = self._db.rawQuery(sql, null);
            }
        } catch (err) {
            callback(err, null);
            return;
        }

        // No Records
        // noinspection JSUnresolvedFunction
        if (cursor.getCount() === 0) {
            cursor.close();
            if (hasCallback) {
                callback(null, null);
            }
            resolve(null);
            return;
        }

        let results;
        const resultEngine = self._getResultEngine(mode);
        try {
            //noinspection JSUnresolvedFunction
            cursor.moveToFirst();
            results = resultEngine(cursor);
            cursor.close();
        } catch (err) {
            callback(err, null);
            return;
        }
        if (hasCallback) {
            callback(null, results);
        }
        resolve(results);
    });
};

Database.prototype._getResultEngine = function(mode) {
    if (mode == null || mode === 0) return DBGetRowResults;

    let resultType = (mode & Database.RESULTSASARRAY|Database.RESULTSASOBJECT);
    if (resultType === 0) {
        resultType = this._resultType;
    }
    let valueType = (mode & Database.VALUESARENATIVE|Database.VALUESARESTRINGS);
    if (valueType === 0) {
        valueType = this._valuesType;
    }

    if (resultType === Database.RESULTSASOBJECT) {
        if (valueType === Database.VALUESARESTRINGS) {
            return DBGetRowObjectString;
        } else {
            return DBGetRowObjectNative;
        }
    } else {
        if (valueType === Database.VALUESARESTRINGS) {
            return DBGetRowArrayString;
        } else {
            return DBGetRowArrayNative;
        }
    }

};

/***
 * This returns the entire result set in a array of rows
 * @param sql - Sql to run
 * @param params - optional
 * @param callback - (err, results)
 * @returns Promise
 */
Database.prototype.all = function(sql, params, callback) {
    if (typeof params === 'function') {
        callback = params;
        params = undefined;
    }

    const self = this;
    return new Promise(function(resolve, reject) {
        let hasCallback = true;
        if (typeof callback !== 'function') {
            callback = reject;
            hasCallback = false;
        }

        if (!self._isOpen) {
            callback("SQLITE.ALL - Database is not open");
            return;
        }

        let cursor, count;
        try {
            if (params !== undefined) {
                //noinspection JSUnresolvedFunction
                cursor = self._db.rawQuery(sql, self._toStringArray(params));
            } else {
                //noinspection JSUnresolvedFunction
                cursor = self._db.rawQuery(sql, null);
            }
            // noinspection JSUnresolvedFunction
            count = cursor.getCount();
        } catch (err) {
            callback(err);
            return;
        }

        // No Records
        if (count === 0) {
            cursor.close();
            if (hasCallback) {
                callback(null, []);
            }
            resolve([]);
            return;
        }
        //noinspection JSUnresolvedFunction
        cursor.moveToFirst();

        let results = [];
        try {
            for (let i = 0; i < count; i++) {
                const data = DBGetRowResults(cursor); // jshint ignore:line
                results.push(data);
                //noinspection JSUnresolvedFunction
                cursor.moveToNext();
            }
            cursor.close();
        } catch (err) {
            callback(err);
            return;
        }
        if (hasCallback) {
            callback(null, results);
        }
        resolve(results);
    });
};

/***
 * This sends each row of the result set to the "Callback" and at the end calls the complete callback upon completion
 * @param sql - sql to run
 * @param params - optional
 * @param callback - callback (err, rowsResult)
 * @param complete - callback (err, recordCount)
 * @returns Promise
 */
Database.prototype.each = function(sql, params, callback, complete) {
    if (typeof params === 'function') {
        complete = callback;
        callback = params;
        params = undefined;
    }

    // Callback is required
    if (typeof callback !== 'function') {
        throw new Error("SQLITE.EACH - requires a callback");
    }

    const self = this;
    return new Promise(function (resolve, reject) {

        // Set the error Callback
        let errorCB = complete || callback;

        let cursor, count;
        try {
            if (params !== undefined) {
                //noinspection JSUnresolvedFunction
                cursor = self._db.rawQuery(sql, self._toStringArray(params));
            } else {
                //noinspection JSUnresolvedFunction
                cursor = self._db.rawQuery(sql, null);
            }
            // noinspection JSUnresolvedFunction
            count = cursor.getCount();
        } catch (err) {
            errorCB(err, null);
            reject(err);
            return;
        }

        // No Records
        if (count === 0) {
            cursor.close();
            if (complete) {
                complete(null, 0);
            }
            resolve(0);
            return;
        }
        //noinspection JSUnresolvedFunction
        cursor.moveToFirst();

        try {
            for (let i = 0; i < count; i++) {
                const data = DBGetRowResults(cursor); // jshint ignore:line
                callback(null, data);
                //noinspection JSUnresolvedFunction
                cursor.moveToNext();
            }
            cursor.close();
        } catch (err) {
            errorCB(err, null);
            reject(err);
            return;
        }
        if (complete) {
            complete(null, count);
        }
        resolve(count);
    });
};

/***
 * Converts a Mixed Array to a String Array
 * @param params
 * @returns {Array}
 * @private
 */
Database.prototype._toStringArray = function(params) {
    let stringParams = [];
    if (Object.prototype.toString.apply(params) === '[object Array]') {
        const count = params.length;
        for (let i=0; i<count; ++i) {
            if (params[i] == null) { // jshint ignore:line
                stringParams.push(null);
            } else {
                stringParams.push(params[i].toString());
            }
        }
    } else {
        if (params == null) { // jshint ignore:line
            stringParams.push(null);
        } else {
            stringParams.push(params.toString());
        }
    }
    return stringParams;
};

/***
 * Is this a SQLite object
 * @param obj - possible sqlite object to check
 * @returns {boolean}
 */
Database.isSqlite = function(obj) {
    return obj && obj._isSqlite;
};

/**
 * Does this database exist on disk
 * @param name
 * @returns {*}
 */
Database.exists = function(name) {
    //noinspection JSUnresolvedFunction
    const dbName = _getContext().getDatabasePath(name).getAbsolutePath();
    // noinspection JSUnresolvedFunction,JSUnresolvedVariable
    const dbFile = new java.io.File(dbName);
    // noinspection JSUnresolvedFunction
    return dbFile.exists();
};

/**
 * Delete the database file if it exists
 * @param name
 */
Database.deleteDatabase = function(name) {
    //noinspection JSUnresolvedFunction
    const dbName = _getContext().getDatabasePath(name).getAbsolutePath();
    // noinspection JSUnresolvedFunction,JSUnresolvedVariable
    let dbFile = new java.io.File(dbName);
    if (dbFile.exists()) {
        dbFile.delete();
        // noinspection JSUnresolvedFunction,JSUnresolvedVariable
        dbFile = new java.io.File(dbName + '-journal');
        if (dbFile.exists()) {
            dbFile.delete();
        }
    }
};

/**
 * Copy the database from the install location
 * @param name
 */
Database.copyDatabase = function(name) {

    //Open your local db as the input stream
    let myInput;
    try {
        // Attempt to use the local app directory version
        // noinspection JSUnresolvedFunction,JSUnresolvedVariable
        myInput = new java.io.FileInputStream(fsModule.knownFolders.currentApp().path + '/' + name);
    }
    catch (err) {
        // Use the Assets version
        // noinspection JSUnresolvedFunction
        myInput = _getContext().getAssets().open("app/"+name);
    }

    if (name.indexOf('/')) {
        name = name.substring(name.lastIndexOf('/')+1);
    }

    //noinspection JSUnresolvedFunction
    const dbName = _getContext().getDatabasePath(name).getAbsolutePath();
    const path = dbName.substr(0, dbName.lastIndexOf('/') + 1);

    // Create "databases" folder if it is missing.  This causes issues on Emulators if it is missing
    // So we create it if it is missing

    try {
        // noinspection JSUnresolvedFunction,JSUnresolvedVariable
        const javaFile = new java.io.File(path);
        //noinspection JSUnresolvedFunction
        if (!javaFile.exists()) {
            //noinspection JSUnresolvedFunction
            javaFile.mkdirs();
            //noinspection JSUnresolvedFunction
            javaFile.setReadable(true);
            //noinspection JSUnresolvedFunction
            javaFile.setWritable(true);
        }
    }
    catch (err) {
        console.info("SQLITE - COPYDATABASE - Creating DB Folder Error", err);
    }

    //Open the empty db as the output stream
    // noinspection JSUnresolvedFunction,JSUnresolvedVariable
    const myOutput = new java.io.FileOutputStream(dbName);

    let success = true;
    try {
        //transfer bytes from the input file to the output file
        //noinspection JSUnresolvedFunction,JSUnresolvedVariable
        let buffer = java.lang.reflect.Array.newInstance(java.lang.Byte.class.getField("TYPE").get(null), 1024);
        let length;
        while ((length = myInput.read(buffer)) > 0) {
            // noinspection JSUnresolvedFunction
            myOutput.write(buffer, 0, length);
        }
    }
    catch (err) {
        success = false;
    }

    //Close the streams
    // noinspection JSUnresolvedFunction
    myOutput.flush();
    // noinspection JSUnresolvedFunction
    myOutput.close();
    myInput.close();
    return success;
};

// Literal Defines
Database.RESULTSASARRAY  = 1;
Database.RESULTSASOBJECT = 2;
Database.VALUESARENATIVE = 4;
Database.VALUESARESTRINGS = 8;

TryLoadingCommercialPlugin();
TryLoadingEncryptionPlugin();

module.exports = Database;

/**
 * gets the current application context
 * @returns {*}
 * @private
 */
function _getContext() {
    if (appModule.android.context) {
        return (appModule.android.context);
    }
    if (typeof appModule.getNativeApplication === 'function') {
        let ctx = appModule.getNativeApplication();
        if (ctx) {
            return ctx;
        }
    }

    //noinspection JSUnresolvedFunction,JSUnresolvedVariable
    ctx = java.lang.Class.forName("android.app.AppGlobals").getMethod("getInitialApplication", null).invoke(null, null);
    if (ctx) return ctx;

    //noinspection JSUnresolvedFunction,JSUnresolvedVariable
    ctx = java.lang.Class.forName("android.app.ActivityThread").getMethod("currentApplication", null).invoke(null, null);
    if (ctx) return ctx;

    return ctx;
}

/** Uses a SQLite Plugin **/
function UsePlugin(loadedSrc, DBModule) {
		if (loadedSrc.prototypes) {
			for (let key in loadedSrc.prototypes) {
			    if (!loadedSrc.prototypes.hasOwnProperty(key)) { continue; }
				DBModule.prototype[key] = loadedSrc.prototypes[key];
			}
		}
		if (loadedSrc.statics) {
			for (let key in loadedSrc.statics) {
                if (!loadedSrc.statics.hasOwnProperty(key)) { continue; }
                DBModule[key] = loadedSrc.statics[key];
			}
		}
		if (typeof loadedSrc.init === 'function') {
			_DatabasePluginInits.push(loadedSrc.init);
		}
}

function TryLoadingCommercialPlugin() {
	try {
		const sqlCom = require('nativescript-sqlite-commercial');
		UsePlugin(sqlCom, Database);
	}
	catch (e) { /* Do Nothing if it doesn't exist as it is an optional plugin */
	}
}

function TryLoadingEncryptionPlugin() {
	try {
		const sqlEnc = require('nativescript-sqlite-encrypted');
		UsePlugin(sqlEnc, Database);
	}
	catch (e) { /* Do Nothing if it doesn't exist as it is an optional plugin */
	}
}