/**
 * Usage for accepting signatures:
 *  $('.sigPad').signaturePad()
 *
 * Usage for displaying previous signatures:
 *  $('.sigPad').signaturePad({displayOnly:true}).regenerate(sig)
 *  or
 *  var api = $('.sigPad').signaturePad({displayOnly:true})
 *  api.regenerate(sig)
 */
(function ($) {

function SignaturePad (selector, options) {
  /**
   * Reference to the object for use in public methods
   *
   * @private
   *
   * @type {Object}
   */
  var self = this

  /**
   * Holds the merged default settings and user passed settings
   *
   * @private
   *
   * @type {Object}
   */
  , settings = $.extend({}, $.fn.signaturePad.defaults, options)

  /**
   * The current context, as passed by jQuery, of selected items
   *
   * @private
   *
   * @type {Object}
   */
  , context = $(selector)

  /**
   * jQuery reference to the canvas element inside the signature pad
   *
   * @private
   *
   * @type {Object}
   */
  , canvas = $(settings.canvas, context)

  /**
   * Dom reference to the canvas element inside the signature pad
   *
   * @private
   *
   * @type {Object}
   */
  , element = canvas.get(0)

  /**
   * The drawing context for the signature canvas
   *
   * @private
   *
   * @type {Object}
   */
  , canvasContext = null

  /**
   * Holds the previous point of drawing
   * Disallows drawing over the same location to make lines more delicate
   *
   * @private
   *
   * @type {Object}
   */
  , previous = {'x': null, 'y': null}

  /**
   * An array holding all the points and lines to generate the signature
   * Each item is an object like:
   * {
   *   mx: moveTo x coordinate
   *   my: moveTo y coordinate
   *   lx: lineTo x coordinate
   *   lx: lineTo y coordinate
   * }
   *
   * @private
   *
   * @type {Array}
   */
  , output = []

  /**
   * Stores a timeout for when the mouse leaves the canvas
   * If the mouse has left the canvas for a specific amount of time
   * Stops drawing on the canvas
   *
   * @private
   *
   * @type {Object}
   */
  , mouseLeaveTimeout = false

    /**
     * Whether the mouse button is currently pressed down or not
     *
     * @private
     *
     * @type {Boolean}
     */
  , mouseButtonDown = false

  /**
   * Whether the browser is a touch event browser or not
   *
   * @private
   *
   * @type {Boolean}
   */
  , touchable = false

  /**
   * Whether events have already been bound to the canvas or not
   *
   * @private
   *
   * @type {Boolean}
   */
  , eventsBound = false

  /**
   * Remembers the default font-size when typing, and will allow it to be scaled for bigger/smaller names
   *
   * @private
   *
   * @type {Number}
   */
  , typeItDefaultFontSize = 30

  /**
   * Remembers the current font-size when typing
   *
   * @private
   *
   * @type {Number}
   */
  , typeItCurrentFontSize = typeItDefaultFontSize

  /**
   * Remembers how many characters are in the name field, to help with the scaling feature
   *
   * @private
   *
   * @type {Number}
   */
  , typeItNumChars = 0


  /**
   * Clears the mouseLeaveTimeout
   * Resets some other variables that may be active
   *
   * @private
   */
  function clearMouseLeaveTimeout () {
    clearTimeout(mouseLeaveTimeout)
    mouseLeaveTimeout = false
    mouseButtonDown = false
  }

  /**
   * Draws a line on canvas using the mouse position
   * Checks previous position to not draw over top of previous drawing
   *  (makes the line really thick and poorly anti-aliased)
   *
   * @private
   *
   * @param {Object} e The event object
   * @param {Number} newYOffset A pixel value for drawing the newY, used for drawing a single dot on click
   */
  function drawLine (e, newYOffset) {
    var offset, newX, newY

    e.preventDefault()

    offset = $(e.target).offset()

    clearTimeout(mouseLeaveTimeout)
    mouseLeaveTimeout = false

    if (typeof e.targetTouches !== 'undefined') {
      newX = Math.floor(e.targetTouches[0].pageX - offset.left)
      newY = Math.floor(e.targetTouches[0].pageY - offset.top)
    } else {
      newX = Math.floor(e.pageX - offset.left)
      newY = Math.floor(e.pageY - offset.top)
    }

    if (previous.x === newX && previous.y === newY)
      return true

    if (previous.x === null)
      previous.x = newX

    if (previous.y === null)
      previous.y = newY

    if (newYOffset)
      newY += newYOffset

    canvasContext.beginPath()
    canvasContext.moveTo(previous.x, previous.y)
    canvasContext.lineTo(newX, newY)
    canvasContext.lineCap = settings.penCap
    canvasContext.stroke()
    canvasContext.closePath()

    output.push({
      'lx' : newX
      , 'ly' : newY
      , 'mx' : previous.x
      , 'my' : previous.y
    })

    previous.x = newX
    previous.y = newY

    if (settings.onDraw && typeof settings.onDraw === 'function')
      settings.onDraw.apply(self)
  }

  /**
   * Callback wrapper for executing stopDrawing without the event
   * Put up here so that it can be removed at a later time
   *
   * @private
   */
  function stopDrawingWrapper () {
    stopDrawing()
  }

  /**
   * Callback registered to mouse/touch events of the canvas
   * Stops the drawing abilities
   *
   * @private
   *
   * @param {Object} e The event object
   */
  function stopDrawing (e) {
    if (!!e) {
      drawLine(e, 1)
    } else {
      if (touchable) {
        canvas.each(function () {
          this.removeEventListener('touchmove', drawLine)
          // this.removeEventListener('MSPointerMove', drawLine)
        })
      } else {
        canvas.unbind('mousemove.signaturepad')
      }

      if (output.length > 0 && settings.onDrawEnd && typeof settings.onDrawEnd === 'function')
        settings.onDrawEnd.apply(self)
    }

    previous.x = null
    previous.y = null

    if (settings.output && output.length > 0)
      $(settings.output, context).val(JSON.stringify(output))
  }

  /**
   * Draws the signature line
   *
   * @private
   */
  function drawSigLine () {
    if (!settings.lineWidth)
      return false

    canvasContext.beginPath()
    canvasContext.lineWidth = settings.lineWidth
    canvasContext.strokeStyle = settings.lineColour
    canvasContext.moveTo(settings.lineMargin, settings.lineTop)
    canvasContext.lineTo(element.width - settings.lineMargin, settings.lineTop)
    canvasContext.stroke()
    canvasContext.closePath()
  }

  /**
   * Clears all drawings off the canvas and redraws the signature line
   *
   * @private
   */
  function clearCanvas () {
    canvasContext.clearRect(0, 0, element.width, element.height)
    canvasContext.fillStyle = settings.bgColour
    canvasContext.fillRect(0, 0, element.width, element.height)

    if (!settings.displayOnly)
      drawSigLine()

    canvasContext.lineWidth = settings.penWidth
    canvasContext.strokeStyle = settings.penColour

    $(settings.output, context).val('')
    output = []

    stopDrawing()
  }

  /**
   * Callback registered to mouse/touch events of the canvas
   * Draws a line at the mouse cursor location, starting a new line if necessary
   *
   * @private
   *
   * @param {Object} e The event object
   * @param {Object} o The object context registered to the event; canvas
   */
  function onMouseMove(e, o) {
    if (previous.x == null) {
      drawLine(e, 1)
    } else {
      drawLine(e, o)
    }
  }

  /**
   * Callback registered to mouse/touch events of canvas
   * Triggers the drawLine function
   *
   * @private
   *
   * @param {Object} e The event object
   * @param {Object} touchObject The object context registered to the event; canvas
   */
  function startDrawing (e, touchObject) {
    if (touchable) {
      touchObject.addEventListener('touchmove', onMouseMove, false)
      // touchObject.addEventListener('MSPointerMove', onMouseMove, false)
    } else {
      canvas.bind('mousemove.signaturepad', onMouseMove)
    }

    // Draws a single point on initial mouse down, for people with periods in their name
    drawLine(e, 1)
  }

  /**
   * Removes all the mouse events from the canvas
   *
   * @private
   */
  function disableCanvas () {
    eventsBound = false

    canvas.each(function () {
      if (this.removeEventListener) {
        this.removeEventListener('touchend', stopDrawingWrapper)
        this.removeEventListener('touchcancel', stopDrawingWrapper)
        this.removeEventListener('touchmove', drawLine)
        this.removeEventListener('touchstart', startTouchDrawing)
        // this.removeEventListener('MSPointerUp', stopDrawingWrapper)
        // this.removeEventListener('MSPointerCancel', stopDrawingWrapper)
        // this.removeEventListener('MSPointerMove', drawLine)
      }
       })
    $(document).unbind('mouseup.signaturepad')
    canvas.unbind('mousedown.signaturepad')
    canvas.unbind('mousemove.signaturepad')
    canvas.unbind('mouseleave.signaturepad')

    $(settings.clear, context).unbind('click.signaturepad')
  }

  /**
   * Lazy touch event detection
   * Uses the first press on the canvas to detect either touch or mouse reliably
   * Will then bind other events as needed
   *
   * @private
   *
   * @param {Object} e The event object
   */
  function initDrawEvents (e) {
    if (eventsBound)
      return false

    eventsBound = true

    // Closes open keyboards to free up space
    $('input').blur();

    if (typeof e.targetTouches !== 'undefined')
      touchable = true

    if (touchable) {
      canvas.each(function () {
        this.addEventListener('touchend', stopDrawingWrapper, false)
        this.addEventListener('touchcancel', stopDrawingWrapper, false)
        // this.addEventListener('MSPointerUp', stopDrawingWrapper, false)
        // this.addEventListener('MSPointerCancel', stopDrawingWrapper, false)
      })

      canvas.unbind('mousedown.signaturepad')
    } else {
      $(document).bind('mouseup.signaturepad', function () {
        if (mouseButtonDown) {
          stopDrawing()
          clearMouseLeaveTimeout()
        }
      })
      canvas.bind('mouseleave.signaturepad', function (e) {
        if (mouseButtonDown) stopDrawing(e)

        if (mouseButtonDown && !mouseLeaveTimeout) {
          mouseLeaveTimeout = setTimeout(function () {
            stopDrawing()
            clearMouseLeaveTimeout()
          }, 500)
        }
      })

      canvas.each(function () {
        this.removeEventListener('touchstart', startTouchDrawing)
      })
    }
  }
  function startTouchDrawing(e) {
      e.preventDefault()
      mouseButtonDown = true
      initDrawEvents(e)
      startDrawing(e, this)

  }
  /**
   * Triggers the abilities to draw on the canvas
   * Sets up mouse/touch events, hides and shows descriptions and sets current classes
   *
   * @private
   */
  function drawIt () {
    $(settings.typed, context).hide()
    clearCanvas()

    canvas.each(function () {
        this.addEventListener('touchstart', startTouchDrawing)
    })

    canvas.bind('mousedown.signaturepad', function (e) {
      e.preventDefault()

      // Only allow left mouse clicks to trigger signature drawing
      if (e.which > 1) return false

      mouseButtonDown = true
      initDrawEvents(e)
      startDrawing(e)
    })

    $(settings.clear, context).bind('click.signaturepad', function (e) { e.preventDefault(); clearCanvas() })

    $(settings.typeIt, context).bind('click.signaturepad', function (e) { e.preventDefault(); typeIt() })
    $(settings.drawIt, context).unbind('click.signaturepad')
    $(settings.drawIt, context).bind('click.signaturepad', function (e) { e.preventDefault() })

    $(settings.typeIt, context).removeClass(settings.currentClass)
    $(settings.drawIt, context).addClass(settings.currentClass)
    $(settings.sig, context).addClass(settings.currentClass)

    $(settings.typeItDesc, context).hide()
    $(settings.drawItDesc, context).show()
    $(settings.clear, context).show()
  }

  /**
   * Triggers the abilities to type in the input for generating a signature
   * Sets up mouse events, hides and shows descriptions and sets current classes
   *
   * @private
   */
  function typeIt () {
    clearCanvas()
    disableCanvas()
    $(settings.typed, context).show()

    $(settings.drawIt, context).bind('click.signaturepad', function (e) { e.preventDefault(); drawIt() })
    $(settings.typeIt, context).unbind('click.signaturepad')
    $(settings.typeIt, context).bind('click.signaturepad', function (e) { e.preventDefault() })

    $(settings.output, context).val('')

    $(settings.drawIt, context).removeClass(settings.currentClass)
    $(settings.typeIt, context).addClass(settings.currentClass)
    $(settings.sig, context).removeClass(settings.currentClass)

    $(settings.drawItDesc, context).hide()
    $(settings.clear, context).hide()
    $(settings.typeItDesc, context).show()

    typeItCurrentFontSize = typeItDefaultFontSize = $(settings.typed, context).css('font-size').replace(/px/, '')
  }

  /**
   * Callback registered on key up and blur events for input field
   * Writes the text fields value as Html into an element
   *
   * @private
   *
   * @param {String} val The value of the input field
   */
  function type (val) {
    var typed = $(settings.typed, context)
      , cleanedVal = $.trim(val.replace(/>/g, '&gt;').replace(/</g, '&lt;'))
      , oldLength = typeItNumChars
      , edgeOffset = typeItCurrentFontSize * 0.5

    typeItNumChars = cleanedVal.length
    typed.html(cleanedVal)

    if (!cleanedVal) {
      typed.css('font-size', typeItDefaultFontSize + 'px')
      return
    }

    if (typeItNumChars > oldLength && typed.outerWidth() > element.width) {
      while (typeItCurrentFontSize > 4 && typed.outerWidth() > element.width) {
        typeItCurrentFontSize--
        typed.css('font-size', typeItCurrentFontSize + 'px')
      }
    }

    if (typeItNumChars < oldLength && typed.outerWidth() + edgeOffset < element.width && typeItCurrentFontSize < typeItDefaultFontSize) {
      while (typeItCurrentFontSize < 512 && typed.outerWidth() + edgeOffset < element.width && typeItCurrentFontSize < typeItDefaultFontSize) {
        typeItCurrentFontSize++
        typed.css('font-size', typeItCurrentFontSize + 'px')
      }
    }
  }

  /**
   * Default onBeforeValidate function to clear errors
   *
   * @private
   *
   * @param {Object} context current context object
   * @param {Object} settings provided settings
   */
  function onBeforeValidate (context, settings) {
    $('p.' + settings.errorClass, context).remove()
    context.removeClass(settings.errorClass)
    $('input, label', context).removeClass(settings.errorClass)
  }

  /**
   * Default onFormError function to show errors
   *
   * @private
   *
   * @param {Object} errors object contains validation errors (e.g. nameInvalid=true)
   * @param {Object} context current context object
   * @param {Object} settings provided settings
   */
  function onFormError (errors, context, settings) {
    if (errors.nameInvalid) {
      context.prepend(['<p class="', settings.errorClass, '">', settings.errorMessage, '</p>'].join(''))
      $(settings.name, context).focus()
      $(settings.name, context).addClass(settings.errorClass)
      $('label[for=' + $(settings.name).attr('id') + ']', context).addClass(settings.errorClass)
    }

    if (errors.drawInvalid)
      context.prepend(['<p class="', settings.errorClass, '">', settings.errorMessageDraw, '</p>'].join(''))
  }

  /**
   * Validates the form to confirm a name was typed in the field
   * If drawOnly also confirms that the user drew a signature
   *
   * @private
   *
   * @return {Boolean}
   */
  function validateForm () {
    var valid = true
      , errors = {drawInvalid: false, nameInvalid: false}
      , onBeforeArguments = [context, settings]
      , onErrorArguments = [errors, context, settings]

    if (settings.onBeforeValidate && typeof settings.onBeforeValidate === 'function') {
      settings.onBeforeValidate.apply(self,onBeforeArguments)
    } else {
      onBeforeValidate.apply(self, onBeforeArguments)
    }

    if (settings.drawOnly && output.length < 1) {
      errors.drawInvalid = true
      valid = false
    }

    if ($(settings.name, context).val() === '') {
      errors.nameInvalid = true
      valid = false
    }

    if (settings.onFormError && typeof settings.onFormError === 'function') {
      settings.onFormError.apply(self,onErrorArguments)
    } else {
      onFormError.apply(self, onErrorArguments)
    }

    return valid
  }

  /**
   * Redraws the signature on a specific canvas
   *
   * @private
   *
   * @param {Array} paths the signature JSON
   * @param {Object} context the canvas context to draw on
   * @param {Boolean} saveOutput whether to write the path to the output array or not
   */
  function drawSignature (paths, context, saveOutput) {
    for(var i in paths) {
      if (typeof paths[i] === 'object') {
        context.beginPath()
        context.moveTo(paths[i].mx, paths[i].my)
        context.lineTo(paths[i].lx, paths[i].ly)
        context.lineCap = settings.penCap
        context.stroke()
        context.closePath()

        if (saveOutput && paths[i].lx) {
          output.push({
            'lx' : paths[i].lx
            , 'ly' : paths[i].ly
            , 'mx' : paths[i].mx
            , 'my' : paths[i].my
          })
        }
      }
    }
  }

  /**
   * Initialisation function, called immediately after all declarations
   * Technically public, but only should be used internally
   *
   * @private
   */
  function init () {
    // Fixes the jQuery.fn.offset() function for Mobile Safari Browsers i.e. iPod Touch, iPad and iPhone
    // https://gist.github.com/661844
    // http://bugs.jquery.com/ticket/6446
    if (parseFloat(((/CPU.+OS ([0-9_]{3}).*AppleWebkit.*Mobile/i.exec(navigator.userAgent)) || [0,'4_2'])[1].replace('_','.')) < 4.1) {
       $.fn.Oldoffset = $.fn.offset;
       $.fn.offset = function () {
          var result = $(this).Oldoffset()
          result.top -= window.scrollY
          result.left -= window.scrollX

          return result
       }
    }

    // Disable selection on the typed div and canvas
    $(settings.typed, context).bind('selectstart.signaturepad', function (e) { return $(e.target).is(':input') })
    canvas.bind('selectstart.signaturepad', function (e) { return $(e.target).is(':input') })

    if (!element.getContext && FlashCanvas)
      FlashCanvas.initElement(element)

    if (element.getContext) {
      canvasContext = element.getContext('2d')

      $(settings.sig, context).show()

      if (!settings.displayOnly) {
        if (!settings.drawOnly) {
          $(settings.name, context).bind('keyup.signaturepad', function () {
            type($(this).val())
          })

          $(settings.name, context).bind('blur.signaturepad', function () {
            type($(this).val())
          })

          $(settings.drawIt, context).bind('click.signaturepad', function (e) {
            e.preventDefault()
            drawIt()
          })
        }

        if (settings.drawOnly || settings.defaultAction === 'drawIt') {
          drawIt()
        } else {
          typeIt()
        }

        if (settings.validateFields) {
          if ($(selector).is('form')) {
            $(selector).bind('submit.signaturepad', function () { return validateForm() })
          } else {
            $(selector).parents('form').bind('submit.signaturepad', function () { return validateForm() })
          }
        }

        $(settings.sigNav, context).show()
      }
    }
  }

  $.extend(self, {
    /**
     * A property to store the current version of Signature Pad
     */
    signaturePad : '{{version}}'

    /**
     * Initializes SignaturePad
     */
    , init : function () { init() }

    /**
     * Allows options to be updated after initialization
     *
     * @param {Object} options An object containing the options to be changed
     */
    , updateOptions : function (options) {
      $.extend(settings, options)
    }

    /**
     * Regenerates a signature on the canvas using an array of objects
     * Follows same format as object property
     * @see var object
     *
     * @param {Array} paths An array of the lines and points
     */
    , regenerate : function (paths) {
      self.clearCanvas()
      $(settings.typed, context).hide()

      if (typeof paths === 'string')
        paths = JSON.parse(paths)

      drawSignature(paths, canvasContext, true)

      if (settings.output && $(settings.output, context).length > 0)
        $(settings.output, context).val(JSON.stringify(output))
    }

    /**
     * Clears the canvas
     * Redraws the background colour and the signature line
     */
    , clearCanvas : function () { clearCanvas() }

    /**
     * Returns the signature as a Js array
     *
     * @return {Array}
     */
    , getSignature : function () { return output }

    /**
     * Returns the signature as a Json string
     *
     * @return {String}
     */
    , getSignatureString : function () { return JSON.stringify(output) }

    /**
     * Returns the signature as an image
     * Re-draws the signature in a shadow canvas to create a clean version
     *
     * @return {String}
     */
    , getSignatureImage : function () {
      var tmpCanvas = document.createElement('canvas')
        , tmpContext = null
        , data = null

      tmpCanvas.style.position = 'absolute'
      tmpCanvas.style.top = '-999em'
      tmpCanvas.width = element.width
      tmpCanvas.height = element.height
      document.body.appendChild(tmpCanvas)

      if (!tmpCanvas.getContext && FlashCanvas)
        FlashCanvas.initElement(tmpCanvas)

      tmpContext = tmpCanvas.getContext('2d')

      tmpContext.fillStyle = settings.bgColour
      tmpContext.fillRect(0, 0, element.width, element.height)
      tmpContext.lineWidth = settings.penWidth
      tmpContext.strokeStyle = settings.penColour

      drawSignature(output, tmpContext)
      data = tmpCanvas.toDataURL.apply(tmpCanvas, arguments)

      document.body.removeChild(tmpCanvas)
      tmpCanvas = null

      return data
    }

    /**
     * The form validation function
     * Validates that the signature has been filled in properly
     * Allows it to be hooked into another validation function and called at a different time
     *
     * @return {Boolean}
     */
    , validateForm : function () { return validateForm() }
  })
}

/**
 * Create the plugin
 * Returns an Api which can be used to call specific methods
 *
 * @param {Object} options The options array
 *
 * @return {Object} The Api for controlling the instance
 */
$.fn.signaturePad = function (options) {
  var api = null

  this.each(function () {
    if (!$.data(this, 'plugin-signaturePad')) {
      api = new SignaturePad(this, options)
      api.init()
      $.data(this, 'plugin-signaturePad', api)
    } else {
      api = $.data(this, 'plugin-signaturePad')
      api.updateOptions(options)
    }
  })

  return api
}

/**
 * Expose the defaults so they can be overwritten for multiple instances
 *
 * @type {Object}
 */
$.fn.signaturePad.defaults = {
  defaultAction : 'typeIt' // What action should be highlighted first: typeIt or drawIt
  , displayOnly : false // Initialize canvas for signature display only; ignore buttons and inputs
  , drawOnly : false // Whether the to allow a typed signature or not
  , canvas : 'canvas' // Selector for selecting the canvas element
  , sig : '.sig' // Parts of the signature form that require Javascript (hidden by default)
  , sigNav : '.sigNav' // The TypeIt/DrawIt navigation (hidden by default)
  , bgColour : '#ffffff' // The colour fill for the background of the canvas; or transparent
  , penColour : '#145394' // Colour of the drawing ink
  , penWidth : 2 // Thickness of the pen
  , penCap : 'round' // Determines how the end points of each line are drawn (values: 'butt', 'round', 'square')
  , lineColour : '#ccc' // Colour of the signature line
  , lineWidth : 2 // Thickness of the signature line
  , lineMargin : 5 // Margin on right and left of signature line
  , lineTop : 35 // Distance to draw the line from the top
  , name : '.name' // The input field for typing a name
  , typed : '.typed' // The Html element to accept the printed name
  , clear : '.clearButton' // Button for clearing the canvas
  , typeIt : '.typeIt a' // Button to trigger name typing actions (current by default)
  , drawIt : '.drawIt a' // Button to trigger name drawing actions
  , typeItDesc : '.typeItDesc' // The description for TypeIt actions
  , drawItDesc : '.drawItDesc' // The description for DrawIt actions (hidden by default)
  , output : '.output' // The hidden input field for remembering line coordinates
  , currentClass : 'current' // The class used to mark items as being currently active
  , validateFields : true // Whether the name, draw fields should be validated
  , errorClass : 'error' // The class applied to the new error Html element
  , errorMessage : 'Please enter your name' // The error message displayed on invalid submission
  , errorMessageDraw : 'Please sign the document' // The error message displayed when drawOnly and no signature is drawn
  , onBeforeValidate : null // Pass a callback to be used instead of the built-in function
  , onFormError : null // Pass a callback to be used instead of the built-in function
  , onDraw : null // Pass a callback to be used to capture the drawing process
  , onDrawEnd : null // Pass a callback to be exectued after the drawing process
}

}(jQuery));
