$ = window.jQuery or window.Zepto or window.$
$.payment = {}
$.payment.fn = {}
$.fn.payment = (method, args...) ->
  $.payment.fn[method].apply(this, args)

# Utils

defaultFormat = /(\d{1,4})/g

$.payment.cards = cards = [
  # Elo has a lot of bins scattered throughout bin-space. Some are one-off, and
  # some are large ranges. We include the full one-offs here and some range
  # prefixes.
  {
    type: 'elo'
    patterns: [
      401178,
      401179,
      431274,
      438935,
      451416,
      457393,
      457631,
      457632,
      504175,
      506699,
      5067,
      509,
      627780,
      636297,
      636368,
      650,
      6516,
      6550,
    ]
    format: defaultFormat
    length: [16]
    cvcLength: [3]
    luhn: true
  }
  {
    type: 'visaelectron'
    patterns: [
      4026, 417500, 4405, 4508, 4844, 4913, 4917
    ]
    format: defaultFormat
    length: [16]
    cvcLength: [3]
    luhn: true
  }
  {
    type: 'maestro'
    patterns: [
      5018, 502, 503, 506, 56, 58, 639, 6220, 67
    ]
    format: defaultFormat
    length: [12..19]
    cvcLength: [3]
    luhn: true
  }
  {
    type: 'forbrugsforeningen'
    patterns: [600]
    format: defaultFormat
    length: [16]
    cvcLength: [3]
    luhn: true
  }
  {
    type: 'dankort'
    patterns: [5019]
    format: defaultFormat
    length: [16]
    cvcLength: [3]
    luhn: true
  }
  # Credit cards
  {
    type: 'visa'
    patterns: [4]
    format: defaultFormat
    length: [13, 16]
    cvcLength: [3]
    luhn: true
  }
  {
    type: 'mastercard'
    patterns: [
      51, 52, 53, 54, 55,
      22, 23, 24, 25, 26, 27
    ]
    format: defaultFormat
    length: [16]
    cvcLength: [3]
    luhn: true
  }
  {
    type: 'amex'
    patterns: [34, 37]
    format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
    length: [15]
    cvcLength: [3..4]
    luhn: true
  }
  {
    type: 'dinersclub'
    patterns: [30, 36, 38, 39]
    format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/
    length: [14]
    cvcLength: [3]
    luhn: true
  }
  {
    type: 'discover'
    patterns: [60, 64, 65, 622]
    format: defaultFormat
    length: [16]
    cvcLength: [3]
    luhn: true
  }
  {
    type: 'unionpay'
    patterns: [62, 88]
    format: defaultFormat
    length: [16..19]
    cvcLength: [3]
    luhn: false
  }
  {
    type: 'jcb'
    patterns: [35]
    format: defaultFormat
    length: [16]
    cvcLength: [3]
    luhn: true
  }
]

cardFromNumber = (num) ->
  num = (num + '').replace(/\D/g, '')
  for card in cards
    for pattern in card.patterns
      p = pattern + ''
      return card if num.substr(0, p.length) == p

cardFromType = (type) ->
  return card for card in cards when card.type is type

luhnCheck = (num) ->
  odd = true
  sum = 0

  digits = (num + '').split('').reverse()

  for digit in digits
    digit = parseInt(digit, 10)
    digit *= 2 if (odd = !odd)
    digit -= 9 if digit > 9
    sum += digit

  sum % 10 == 0

hasTextSelected = ($target) ->
  # If some text is selected
  return true if $target.prop('selectionStart')? and
    $target.prop('selectionStart') isnt $target.prop('selectionEnd')

  # If some text is selected in IE
  if document?.selection?.createRange?
    return true if document.selection.createRange().text

  false

# Private

# Safe Val

safeVal = (value, $target) ->
  try
    cursor = $target.prop('selectionStart')
  catch error
    cursor = null
  last = $target.val()
  $target.val(value)
  if cursor != null && $target.is(":focus")
    cursor = value.length if cursor is last.length

    # This hack looks for scenarios where we are changing an input's value such
    # that "X| " is replaced with " |X" (where "|" is the cursor). In those
    # scenarios, we want " X|".
    #
    # For example:
    # 1. Input field has value "4444| "
    # 2. User types "1"
    # 3. Input field has value "44441| "
    # 4. Reformatter changes it to "4444 |1"
    # 5. By incrementing the cursor, we make it "4444 1|"
    #
    # This is awful, and ideally doesn't go here, but given the current design
    # of the system there does not appear to be a better solution.
    #
    # Note that we can't just detect when the cursor-1 is " ", because that
    # would incorrectly increment the cursor when backspacing, e.g. pressing
    # backspace in this scenario: "4444 1|234 5".
    if last != value
      prevPair = last[cursor-1..cursor]
      currPair = value[cursor-1..cursor]
      digit = value[cursor]
      cursor = cursor + 1 if /\d/.test(digit) and
        prevPair == "#{digit} " and currPair == " #{digit}"

    $target.prop('selectionStart', cursor)
    $target.prop('selectionEnd', cursor)

# Replace Full-Width Chars

replaceFullWidthChars = (str = '') ->
  fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
  halfWidth = '0123456789'

  value = ''
  chars = str.split('')

  # Avoid using reserved word `char`
  for chr in chars
    idx = fullWidth.indexOf(chr)
    chr = halfWidth[idx] if idx > -1
    value += chr

  value

# Format Numeric

reFormatNumeric = (e) ->
  $target = $(e.currentTarget)
  setTimeout ->
    value   = $target.val()
    value   = replaceFullWidthChars(value)
    value   = value.replace(/\D/g, '')
    safeVal(value, $target)

# Format Card Number

reFormatCardNumber = (e) ->
  $target = $(e.currentTarget)
  setTimeout ->
    value   = $target.val()
    value   = replaceFullWidthChars(value)
    value   = $.payment.formatCardNumber(value)
    safeVal(value, $target)

formatCardNumber = (e) ->
  # Only format if input is a number
  digit = String.fromCharCode(e.which)
  return unless /^\d+$/.test(digit)

  $target = $(e.currentTarget)
  value   = $target.val()
  card    = cardFromNumber(value + digit)
  length  = (value.replace(/\D/g, '') + digit).length

  upperLength = 16
  upperLength = card.length[card.length.length - 1] if card
  return if length >= upperLength

  # Return if focus isn't at the end of the text
  return if $target.prop('selectionStart')? and
    $target.prop('selectionStart') isnt value.length

  if card && card.type is 'amex'
    # AMEX cards are formatted differently
    re = /^(\d{4}|\d{4}\s\d{6})$/
  else
    re = /(?:^|\s)(\d{4})$/

  # If '4242' + 4
  if re.test(value)
    e.preventDefault()
    setTimeout -> $target.val(value + ' ' + digit)

  # If '424' + 2
  else if re.test(value + digit)
    e.preventDefault()
    setTimeout -> $target.val(value + digit + ' ')

formatBackCardNumber = (e) ->
  $target = $(e.currentTarget)
  value   = $target.val()

  # Return unless backspacing
  return unless e.which is 8

  # Return if focus isn't at the end of the text
  return if $target.prop('selectionStart')? and
    $target.prop('selectionStart') isnt value.length

  # Remove the digit + trailing space
  if /\d\s$/.test(value)
    e.preventDefault()
    setTimeout -> $target.val(value.replace(/\d\s$/, ''))
  # Remove digit if ends in space + digit
  else if /\s\d?$/.test(value)
    e.preventDefault()
    setTimeout -> $target.val(value.replace(/\d$/, ''))

# Format Expiry

reFormatExpiry = (e) ->
  $target = $(e.currentTarget)
  setTimeout ->
    value   = $target.val()
    value   = replaceFullWidthChars(value)
    value   = $.payment.formatExpiry(value)
    safeVal(value, $target)

formatExpiry = (e) ->
  # Only format if input is a number
  digit = String.fromCharCode(e.which)
  return unless /^\d+$/.test(digit)

  $target = $(e.currentTarget)
  val     = $target.val() + digit

  if /^\d$/.test(val) and val not in ['0', '1']
    e.preventDefault()
    setTimeout -> $target.val("0#{val} / ")

  else if /^\d\d$/.test(val)
    e.preventDefault()
    setTimeout ->
      # Split for months where we have the second digit > 2 (past 12) and turn
      # that into (m1)(m2) => 0(m1) / (m2)
      m1 = parseInt(val[0], 10)
      m2 = parseInt(val[1], 10)
      if m2 > 2 and m1 != 0
        $target.val("0#{m1} / #{m2}")
      else
        $target.val("#{val} / ")

formatForwardExpiry = (e) ->
  digit = String.fromCharCode(e.which)
  return unless /^\d+$/.test(digit)

  $target = $(e.currentTarget)
  val     = $target.val()

  if /^\d\d$/.test(val)
    $target.val("#{val} / ")

formatForwardSlashAndSpace = (e) ->
  which = String.fromCharCode(e.which)
  return unless which is '/' or which is ' '

  $target = $(e.currentTarget)
  val     = $target.val()

  if /^\d$/.test(val) and val isnt '0'
    $target.val("0#{val} / ")

formatBackExpiry = (e) ->
  $target = $(e.currentTarget)
  value   = $target.val()

  # Return unless backspacing
  return unless e.which is 8

  # Return if focus isn't at the end of the text
  return if $target.prop('selectionStart')? and
    $target.prop('selectionStart') isnt value.length

  # Remove the trailing space + last digit
  if /\d\s\/\s$/.test(value)
    e.preventDefault()
    setTimeout -> $target.val(value.replace(/\d\s\/\s$/, ''))

# Format CVC

reFormatCVC = (e) ->
  $target = $(e.currentTarget)
  setTimeout ->
    value   = $target.val()
    value   = replaceFullWidthChars(value)
    value   = value.replace(/\D/g, '')[0...4]
    safeVal(value, $target)

# Restrictions

restrictNumeric = (e) ->
  # Key event is for a browser shortcut
  return true if e.metaKey or e.ctrlKey

  # If keycode is a space
  return false if e.which is 32

  # If keycode is a special char (WebKit)
  return true if e.which is 0

  # If char is a special char (Firefox)
  return true if e.which < 33

  input = String.fromCharCode(e.which)

  # Char is a number or a space
  !!/[\d\s]/.test(input)

restrictCardNumber = (e) ->
  $target = $(e.currentTarget)
  digit   = String.fromCharCode(e.which)
  return unless /^\d+$/.test(digit)

  return if hasTextSelected($target)

  # Restrict number of digits
  value = ($target.val() + digit).replace(/\D/g, '')
  card  = cardFromNumber(value)

  if card
    value.length <= card.length[card.length.length - 1]
  else
    # All other cards are 16 digits long
    value.length <= 16

restrictExpiry = (e) ->
  $target = $(e.currentTarget)
  digit   = String.fromCharCode(e.which)
  return unless /^\d+$/.test(digit)

  return if hasTextSelected($target)

  value = $target.val() + digit
  value = value.replace(/\D/g, '')

  return false if value.length > 6

restrictCVC = (e) ->
  $target = $(e.currentTarget)
  digit   = String.fromCharCode(e.which)
  return unless /^\d+$/.test(digit)

  return if hasTextSelected($target)

  val     = $target.val() + digit
  val.length <= 4

setCardType = (e) ->
  $target  = $(e.currentTarget)
  val      = $target.val()
  cardType = $.payment.cardType(val) or 'unknown'

  unless $target.hasClass(cardType)
    allTypes = (card.type for card in cards)

    $target.removeClass('unknown')
    $target.removeClass(allTypes.join(' '))

    $target.addClass(cardType)
    $target.toggleClass('identified', cardType isnt 'unknown')
    $target.trigger('payment.cardType', cardType)

# Public

# Formatting

$.payment.fn.formatCardCVC = ->
  @on('keypress', restrictNumeric)
  @on('keypress', restrictCVC)
  @on('paste', reFormatCVC)
  @on('change', reFormatCVC)
  @on('input', reFormatCVC)
  this

$.payment.fn.formatCardExpiry = ->
  @on('keypress', restrictNumeric)
  @on('keypress', restrictExpiry)
  @on('keypress', formatExpiry)
  @on('keypress', formatForwardSlashAndSpace)
  @on('keypress', formatForwardExpiry)
  @on('keydown',  formatBackExpiry)
  @on('change', reFormatExpiry)
  @on('input', reFormatExpiry)
  this

$.payment.fn.formatCardNumber = ->
  @on('keypress', restrictNumeric)
  @on('keypress', restrictCardNumber)
  @on('keypress', formatCardNumber)
  @on('keydown', formatBackCardNumber)
  @on('keyup', setCardType)
  @on('paste', reFormatCardNumber)
  @on('change', reFormatCardNumber)
  @on('input', reFormatCardNumber)
  @on('input', setCardType)
  this

# Restrictions

$.payment.fn.restrictNumeric = ->
  @on('keypress', restrictNumeric)
  @on('paste', reFormatNumeric)
  @on('change', reFormatNumeric)
  @on('input', reFormatNumeric)
  this

# Validations

$.payment.fn.cardExpiryVal = ->
  $.payment.cardExpiryVal($(this).val())

$.payment.cardExpiryVal = (value) ->
  [month, year] = value.split(/[\s\/]+/, 2)

  # Allow for year shortcut
  if year?.length is 2 and /^\d+$/.test(year)
    prefix = (new Date).getFullYear()
    prefix = prefix.toString()[0..1]
    year   = prefix + year

  month = parseInt(month, 10)
  year  = parseInt(year, 10)

  month: month, year: year

$.payment.validateCardNumber = (num) ->
  num = (num + '').replace(/\s+|-/g, '')
  return false unless /^\d+$/.test(num)

  card = cardFromNumber(num)
  return false unless card

  num.length in card.length and
    (card.luhn is false or luhnCheck(num))

$.payment.validateCardExpiry = (month, year) ->
  # Allow passing an object
  if typeof month is 'object' and 'month' of month
    {month, year} = month

  return false unless month and year

  month = $.trim(month)
  year  = $.trim(year)

  return false unless /^\d+$/.test(month)
  return false unless /^\d+$/.test(year)
  return false unless 1 <= month <= 12

  if year.length == 2
    if year < 70
      year = "20#{year}"
    else
      year = "19#{year}"

  return false unless year.length == 4

  expiry      = new Date(year, month)
  currentTime = new Date

  # Months start from 0 in JavaScript
  expiry.setMonth(expiry.getMonth() - 1)

  # The cc expires at the end of the month,
  # so we need to make the expiry the first day
  # of the month after
  expiry.setMonth(expiry.getMonth() + 1, 1)

  expiry > currentTime

$.payment.validateCardCVC = (cvc, type) ->
  cvc = $.trim(cvc)
  return false unless /^\d+$/.test(cvc)

  card = cardFromType(type)
  if card?
    # Check against a explicit card type
    cvc.length in card.cvcLength
  else
    # Check against all types
    cvc.length >= 3 and cvc.length <= 4

$.payment.cardType = (num) ->
  return null unless num
  cardFromNumber(num)?.type or null

$.payment.formatCardNumber = (num) ->
  num = num.replace(/\D/g, '')
  card = cardFromNumber(num)
  return num unless card

  upperLength = card.length[card.length.length - 1]
  num = num[0...upperLength]

  if card.format.global
    num.match(card.format)?.join(' ')
  else
    groups = card.format.exec(num)
    return unless groups?
    groups.shift()
    groups = $.grep(groups, (n) -> n) # Filter empty groups
    groups.join(' ')

$.payment.formatExpiry = (expiry) ->
  parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
  return '' unless parts

  mon = parts[1] || ''
  sep = parts[2] || ''
  year = parts[3] || ''

  if year.length > 0
    sep = ' / '

  else if sep is ' /'
    mon = mon.substring(0, 1)
    sep = ''

  else if mon.length == 2 or sep.length > 0
    sep = ' / '

  else if mon.length == 1 and mon not in ['0', '1']
    mon = "0#{mon}"
    sep = ' / '

  return mon + sep + year
