var Creditly = (function() {
    var getInputValue = function(e, selector) {
      var inputValue = document.querySelector(selector).value.trim();
      inputValue = inputValue + String.fromCharCode(e.which);
      return getNumber(inputValue);
    };
  
    var getNumber = function(string) {
      return string.replace(/[^\d]/g, "");
    };
  
    var removeErrors = function(element) {
      element.className = element.className.replace(/\bhas-error\b/,'');
    };
  
    var reachedMaximumLength = function(e, maximumLength, selector) {
      return getInputValue(e, selector).length > maximumLength;
    };
  
    // Backspace, delete, tab, escape, enter, ., Ctrl+a, Ctrl+c, Ctrl+v, home, end, left, right
    var isEscapedKeyStroke = function(e) {
      // Key event is for a browser shortcut
      if (e.metaKey || e.ctrlKey) return true;
  
      // If keycode is a space
      if (e.which === 32) return false;
  
      // If keycode is a special char (WebKit)
      if (e.which === 0) return true;
  
      // If char is a special char (Firefox)
      if (e.which < 33) return true;
  
      return false;
    };
  
    var isNumberEvent = function(e) {
      return (/^\d+$/.test(String.fromCharCode(e.which)));
    };
  
    var onlyAllowNumeric = function(e, maximumLength, selector) {
      e.preventDefault();
      // Ensure that it is a number and stop the keypress
      if (!isNumberEvent(e)) {
        return false;
      }
      return true;
    };
  
    var isAmericanExpress = function(number) {
      return number.match("^(34|37)");
    };
  
    var shouldProcessInput = function(e, maximumLength, selector) {
      var target = e.currentTarget;
      if (reachedMaximumLength(e, maximumLength, selector)) {
        e.preventDefault();
        return false;
      }
      if ((target.selectionStart !== target.value.length)) {
        return false;
      }
      return (!isEscapedKeyStroke(e)) && onlyAllowNumeric(e, maximumLength, selector);
    };
  
    var CvvInput = (function() {
  
      var createCvvInput = function(selector, numberSelector) {
        var getMaximumLength = function(isAmericanExpressCard) {
          if (isAmericanExpressCard) {
            return 4;
          } else {
            return 3;
          }
        };
  
        document.querySelector(selector).addEventListener("keypress", function(e) {
          var element = document.querySelector(selector);
          removeErrors(element);
          var number = getInputValue(e, numberSelector);
          var cvv = getInputValue(e, selector)
          var isAmericanExpressCard = isAmericanExpress(number);
          var maximumLength = getMaximumLength(isAmericanExpressCard);
          if (shouldProcessInput(e, maximumLength, selector)) {
            element.value = cvv;
          }
        });
      };
  
      return {
        createCvvInput: createCvvInput
      };
    })();
  
    var NumberInput = (function() {
      var americanExpressSpaces = [4, 10, 15];
      var defaultSpaces = [4, 8, 12, 16];
  
      var getMaximumLength = function(isAmericanExpressCard) {
        if (isAmericanExpressCard) {
          return 15;
        } else {
          return 16;
        }
      };
  
      var createNumberInput = function(selector) {
        document.querySelector(selector).addEventListener("keypress", function(e) {
          var element = document.querySelector(selector);
          removeErrors(element);
          var number = getInputValue(e, selector);
          var isAmericanExpressCard = isAmericanExpress(number);
          var maximumLength = getMaximumLength(isAmericanExpressCard);
          if (shouldProcessInput(e, maximumLength, selector)) {
            var newInput;
            if (isAmericanExpressCard) {
              newInput = addSpaces(number, americanExpressSpaces);
            } else {
              newInput = addSpaces(number, defaultSpaces);
            }
  
            element.value = newInput;
            var ev = document.createEvent("HTMLEvents");
            ev.initEvent("changed_input", true, true);
            element.dispatchEvent(ev);
          }
        });
      };
  
      var addSpaces = function(number, spaces) {
        var parts = []
        var j = 0;
        for (var i=0; i<spaces.length; i++) {
          if (number.length > spaces[i]) {
            parts.push(number.slice(j, spaces[i]));
            j = spaces[i];
          } else {
            if (i < spaces.length) {
              parts.push(number.slice(j, spaces[i]));
            } else {
              parts.push(number.slice(j));
            }
            break;
          }
        }
  
        if (parts.length > 0) {
          return parts.join(" ");
        } else {
          return number;
        }
      };
  
      return {
        createNumberInput: createNumberInput
      };
    })();
  
    var Validation = (function() {
      var Validators = (function() {
        var expirationRegex = /(\d\d)\s*\/\s*(\d\d)/;
  
        var creditCardExpiration = function(selector, data) {
          var expirationVal = document.querySelector(selector).value.trim()
          var match = expirationRegex.exec(expirationVal);
          var isValid = false;
          var outputValue = ["", ""];
          if (match && match.length === 3) {
            var month = parseInt(match[1], 10);
            var year = parseInt("20" + match[2], 10);
            if (month >= 0 && month <= 12) {
              isValid = true;
              var outputValue = [month, year];
            }
          }
  
          return {
            "is_valid": isValid,
            "messages": [data["message"]],
            "output_value": outputValue
          };
        };
  
        var isValidSecurityCode = function(isAmericanExpress, securityCode) {
          if ((isAmericanExpress && securityCode.length == 4) || 
              (!isAmericanExpress && securityCode.length == 3)) {
            return true;
          }
          return false;
        };
  
        var creditCard = function(selector, data) {
          var rawNumber = document.querySelector(data["creditCardNumberSelector"]).value;
          var number = rawNumber.trim().replace(/\D/g, "");
          var messages = [];
          var isValid = true;
          var selectors = [];
  
          if (!isValidCreditCardNumber(number)) {
            messages.push(data["message"]["number"]);
            selectors.push(data["creditCardNumberSelector"]);
            isValid = false;
          }
  
          if (data.cvvSelector) {
            var rawSecurityCode = document.querySelector(data["cvvSelector"]).value;
            var securityCode = rawSecurityCode.trim().replace(/\D/g, "");
  
            if (!isValidSecurityCode(isAmericanExpress(number), securityCode)) {
              messages.push(data["message"]["security_code"]);
              selectors.push(data["cvvSelector"]);
              isValid = false;
            }
          }
  
          result = {
            "is_valid": isValid,
            "output_value": [number, securityCode],
            "selectors": selectors,
            "messages": messages
          };
          return result;
        };
  
        var isAmericanExpress = function(number) {
          return (number.length == 15);
        };
  
        // Luhn Algorithm.
        var isValidCreditCardNumber = function(value) {
          if (value.length === 0) return false;
          // accept only digits, dashes or spaces
          if (/[^0-9-\s]+/.test(value)) return false;
  
          var nCheck = 0, nDigit = 0, bEven = false;
          for (var n = value.length - 1; n >= 0; n--) {
            var cDigit = value.charAt(n);
            var nDigit = parseInt(cDigit, 10);
            if (bEven) {
              if ((nDigit *= 2) > 9) nDigit -= 9;
            }
            nCheck += nDigit;
            bEven = !bEven;
          }
          return (nCheck % 10) == 0;
        };
  
        return {
          creditCard: creditCard,
          creditCardExpiration: creditCardExpiration,
        };
      })();
  
      var ValidationErrorHolder = (function() {
        var errorMessages = [];
        var selectors = [];
  
        var addError = function(selector, validatorResults) {
          if (validatorResults.hasOwnProperty("selectors")) {
            selectors = selectors.concat(validatorResults["selectors"]);
          } else {
            selectors.push(selector)
          }
  
          errorMessages = errorMessages.concat(validatorResults["messages"]);
        };
  
        var triggerErrorMessage = function() {
          var errorsPayload = {
            "selectors": selectors,
            "messages": errorMessages
          };
          for (var i=0; i<selectors.length; i++) {
            document.querySelector(selectors[i]).className = document.querySelector(selectors[i]).className + " has-error";
          }
  
          if (window.jQuery) {
            window.jQuery("body").trigger("creditly_client_validation_error", errorsPayload);
          } else {
            var ev = document.createEvent("MessageEvent");
            ev.initMessageEvent("creditly_client_validation_error", true, true, errorsPayload, "", "", "", null);
            document.body.dispatchEvent(ev);
          }
        };
  
        return {
          addError: addError,
          triggerErrorMessage: triggerErrorMessage
        };
      });
  
      var ValidationOutputHolder = (function() {
        var output = {};
  
        var addOutput = function(outputName, value) {
          var outputParts = outputName.split(".");
          var currentPart = output;
          for (var i=0; i<outputParts.length; i++) {
            if (!currentPart.hasOwnProperty(outputParts[i])) {
              currentPart[outputParts[i]] = {};
            }
  
            // Either place the value into the output, or continue going down the
            // search space.
            if (i === outputParts.length-1) {
              currentPart[outputParts[i]] = value
            } else {
              currentPart = currentPart[outputParts[i]];
            }
          }
        };
  
        var getOutput = function() {
          return output;
        };
  
        return {
          addOutput: addOutput,
          getOutput: getOutput
        }
      });
  
      var processSelector = function(selector, selectorValidatorMap, errorHolder, outputHolder) {
        if (selectorValidatorMap.hasOwnProperty(selector)) {
          var currentMapping = selectorValidatorMap[selector];
          var validatorType = currentMapping["type"];
          var fieldName = currentMapping["name"];
          var validatorResults = Validators[validatorType](selector, currentMapping["data"]);
  
          if (validatorResults["is_valid"]) {
            if (currentMapping["output_name"] instanceof Array) {
              for (var i=0; i<currentMapping["output_name"].length; i++) {
                outputHolder.addOutput(currentMapping["output_name"][i],
                    validatorResults["output_value"][i]);
              }
            } else {
              outputHolder.addOutput(currentMapping["output_name"],
                  validatorResults["output_value"]);
            }
          } else {
            errorHolder.addError(selector, validatorResults);
            return true;
          }
        }
      };
  
      var validate = function(selectorValidatorMap) {
        var errorHolder = ValidationErrorHolder();
        var outputHolder = ValidationOutputHolder();
        var anyErrors = false;
        for (var selector in selectorValidatorMap) {
          if (processSelector(selector, selectorValidatorMap, errorHolder, outputHolder)) {
            anyErrors = true;
          }
        }
        if (anyErrors) {
          errorHolder.triggerErrorMessage();
          return false;
        } else {
          return outputHolder.getOutput();
        }
      };
  
      return {
        validate: validate
      };
    })();
  
    var ExpirationInput = (function() {
      var maximumLength = 4;
  
      var createExpirationInput = function(selector) {
        document.querySelector(selector).addEventListener("keypress", function(e) {
          var element = document.querySelector(selector)
          removeErrors(element);
          if (shouldProcessInput(e, maximumLength, selector)) {
            var inputValue = getInputValue(e, selector);
            if (inputValue.length >= 2) {
              var newInput = inputValue.slice(0, 2) + " / " + inputValue.slice(2);
              element.value = newInput;
            } else {
              element.value = inputValue;
            }
          }
        });
      };
  
      return {
        createExpirationInput: createExpirationInput,
      };
    })();
  
    var CardTypeListener = (function() {
      var determineCardType = function(value) {
        if (/^(34|37)/.test(value)) {
          return "American Express";
        } else if (/^4/.test(value)) {
          return "Visa";
        } else if (/^5[0-5]/.test(value)) {
          return "MasterCard";
        } else if (/^(6011|622|64[4-9]|65)/.test(value)) {
          return "Discover";
        } else {
          return "";
        }
      };
  
      var changeCardType = function(numberSelector, cardTypeSelector) {
        var element = document.querySelector(numberSelector);
        var changeCardTypeListener = function(e) {
          var data = document.querySelector(numberSelector).value;
          var cardType = determineCardType(getNumber(data));
          document.querySelector(cardTypeSelector).innerHTML = cardType;
        };
        element.addEventListener("changed_input", changeCardTypeListener);
        element.addEventListener("keypress", changeCardTypeListener);
        element.addEventListener("keydown", changeCardTypeListener);
        element.addEventListener("keyup", changeCardTypeListener);
      };
  
      return {
        changeCardType: changeCardType
      };
  
    })();
  
    var initialize = function(expirationSelector, creditCardNumberSelector, cvvSelector, cardTypeSelector, options) {
      var selectorValidatorMap = createSelectorValidatorMap(expirationSelector, creditCardNumberSelector, cvvSelector, options);
  
      ExpirationInput.createExpirationInput(expirationSelector);
      NumberInput.createNumberInput(creditCardNumberSelector);
      if (cvvSelector) {
        CvvInput.createCvvInput(cvvSelector, creditCardNumberSelector);
      }
      if (cardTypeSelector) {
        CardTypeListener.changeCardType(creditCardNumberSelector, cardTypeSelector);
      }
  
      return {
        validate: function() {
          return Validation.validate(selectorValidatorMap);
        }
      };
    };
  
    var createSelectorValidatorMap = function(expirationSelector, creditCardNumberSelector, cvvSelector, options) {
      var optionValues = options || {};
      optionValues["security_code_message"] = optionValues["security_code_message"] || "Your security code is invalid";
      optionValues["number_message"] = optionValues["number_message"] || "Your credit card number is invalid";
      optionValues["expiration_message"] = optionValues["expiration_message"] || "Your credit card expiration is invalid";
  
      var selectorValidatorMap = {};
      selectorValidatorMap[creditCardNumberSelector] = {
          "type": "creditCard",
          "data": {
            "cvvSelector": cvvSelector,
            "creditCardNumberSelector": creditCardNumberSelector,
            "message": {
              "security_code": optionValues["security_code_message"],
              "number": optionValues["number_message"]
            }
          },
          "output_name": ["number", "security_code"]
        };
      selectorValidatorMap[expirationSelector] = {
          "type": "creditCardExpiration",
          "data": {
            "message": optionValues["expiration_message"]
          },
          "output_name": ["expiration_month", "expiration_year"]
        };
      return selectorValidatorMap;
    };
  
    return {
      initialize: initialize
    };
  })();
  