###
jQuery Gridly
Copyright 2015 Kevin Sylvestre
1.2.9
###

"use strict"

$ = jQuery

class Animation
  @transitions:
    "webkitTransition": "webkitTransitionEnd"
    "mozTransition": "mozTransitionEnd"
    "oTransition": "oTransitionEnd"
    "transition": "transitionend"

  @transition: ($el) ->
    el = $el[0]
    return result for type, result of @transitions when el.style[type]?

  @execute: ($el, callback) ->
    transition = @transition($el)
    if transition? then $el.one(transition, callback) else callback()

class Draggable

  constructor: ($container, selector, callbacks) ->
    @$container = $container
    @selector = selector
    @callbacks = callbacks
    @toggle()

  bind: (method = 'on') =>
    $(document)[method] 'mousemove touchmove', @moved
    $(document)[method] 'mouseup touchcancel', @ended

  toggle: (method = 'on') =>
    @$container[method] 'mousedown touchstart', @selector, @began
    @$container[method] 'touchend', @selector, @touchend
    @$container[method] 'click', @selector, @click

  on: =>
    @toggle('on')

  off: =>
    @toggle('off')

  coordinate: (event) =>
    switch event.type
      when 'touchstart','touchmove','touchend','touchcancel' then event.originalEvent.touches[0]
      else event

  began: (event) =>
    return if @$target
    event.preventDefault()
    event.stopPropagation()
    @bind('on')

    @$target = $(event.target).closest(@$container.find(@selector))
    @$target.addClass('dragging')

    @origin =
      x: @coordinate(event).pageX - @$target.position().left
      y: @coordinate(event).pageY - @$target.position().top

    @callbacks?.began?(event)

  ended: (event) =>
    return unless @$target?
    if event.type != 'touchend'
        event.preventDefault()
        event.stopPropagation()
    @bind('off')

    @$target.removeClass('dragging')

    delete @$target
    delete @origin

    @callbacks?.ended?(event)

  moved: (event) =>
    return unless @$target?
    event.preventDefault()
    event.stopPropagation()

    @$target.css
      left: @coordinate(event).pageX - @origin.x
      top:  @coordinate(event).pageY - @origin.y

    @dragged = @$target
    @callbacks?.moved?(event)

  click: (event) =>
    return unless @dragged
    event.preventDefault()
    event.stopPropagation()
    delete @dragged

  touchend: (event) =>
    @ended(event)
    @click(event)

class Gridly

  @settings:
    base: 60
    gutter: 20
    columns: 12
    draggable:
      zIndex: 800
      selector : '> *'

  @gridly: ($el, options = {}) ->
    data = $el.data('_gridly')
    if data
      $.extend data.settings, options
    else
      data = new Gridly($el, options)
      $el.data('_gridly', data)
    return data

  constructor: ($el, settings = {}) ->
    @$el = $el
    @settings = $.extend {}, Gridly.settings, settings
    @ordinalize(@$('> *'))
    @draggable() unless @settings.draggable is false
    return @

  ordinalize: ($elements) =>
    for i in [0 .. $elements.length]
      $element = $($elements[i])
      $element.data('position', i)

  reordinalize: ($element, position) =>
    $element.data('position', position)

  $: (selector) =>
    @$el.find(selector)

  compare: (d, s) =>
    return +1 if d.y > s.y + s.h
    return -1 if s.y > d.y + d.h
    return +1 if (d.x + (d.w / 2)) > (s.x + (s.w / 2))
    return -1 if (s.x + (s.w / 2)) > (d.x + (d.w / 2))
    return 0

  draggable: (method) =>
    @_draggable ?= new Draggable @$el, @settings.draggable.selector,
      began: @draggingBegan
      ended: @draggingEnded
      moved: @draggingMoved
    @_draggable[method]() if method?

  $sorted: ($elements) =>
    ($elements || @$('> *')).sort (a,b) ->
      $a = $(a)
      $b = $(b)
      aPosition = $a.data('position')
      bPosition = $b.data('position')
      aPositionInt = parseInt(aPosition)
      bPositionInt = parseInt(bPosition)
      return -1 if aPosition? and not bPosition?
      return +1 if bPosition? and not aPosition?
      return -1 if not aPosition and not bPosition and $a.index() < $b.index()
      return +1 if not bPosition and not aPosition and $b.index() < $a.index()
      return -1 if aPositionInt < bPositionInt
      return +1 if bPositionInt < aPositionInt
      return 0

  draggingBegan: (event) =>
    $elements = @$sorted()
    @ordinalize($elements)
    setTimeout @layout, 0
    @settings?.callbacks?.reordering?($elements)

  draggingEnded: (event) =>
    $elements = @$sorted()
    @ordinalize($elements)
    setTimeout @layout, 0
    @settings?.callbacks?.reordered?($elements, @_draggable.dragged)

  draggingMoved: (event) =>
    $dragging = $(event.target).closest(@$(@settings.draggable.selector))
    $elements = @$sorted(@$(@settings.draggable.selector))
    positions = @structure($elements).positions
    original = index = $dragging.data('position')

    for element in positions.filter((position) -> position.$element.is($dragging))
      element.x = $dragging.position().left
      element.y = $dragging.position().top
      element.w = $dragging.data('width')  || $dragging.outerWidth()
      element.h = $dragging.data('height') || $dragging.outerHeight()

    positions.sort @compare

    $elements = positions.map (position) -> position.$element
    $elements = (@settings.callbacks?.optimize || @optimize)($elements)

    for i in [0...$elements.length]
      @reordinalize($($elements[i]), i)

    @layout()

  size: ($element) =>
    (($element.data('width') || $element.outerWidth()) + @settings.gutter) / (@settings.base + @settings.gutter)

  position: ($element, columns) =>
    size = @size($element)

    height = Infinity
    column = 0

    for i in [0 ... (columns.length - size)]
      max = Math.max columns[i ... (i + size)]...
      if max < height
        height = max
        column = i

    for i in [column ... column + size]
      columns[i] = height + ($element.data('height') || $element.outerHeight()) + @settings.gutter

    x: (column * (@settings.base + @settings.gutter))
    y: height

  structure: ($elements = @$sorted()) =>
    positions = []
    columns = (0 for i in [0 .. @settings.columns])

    for index in [0 ... $elements.length]
      $element = $($elements[index])

      position = @position($element, columns)
      positions.push
        x: position.x
        y: position.y
        w: $element.data('width') || $element.outerWidth()
        h: $element.data('height') || $element.outerHeight()
        $element: $element

    height: Math.max columns...
    positions: positions

  layout: =>
    $elements = (@settings.callbacks?.optimize || @optimize)(@$sorted())

    structure = @structure($elements)

    for index in [0 ... $elements.length]
      $element = $($elements[index])
      position = structure.positions[index]

      continue if $element.is('.dragging')

      $element.css
        position: 'absolute'
        left: position.x
        top:  position.y

    @$el.css
      height: structure.height

  optimize: (originals) =>
    results = []

    columns = 0
    while originals.length > 0
      columns = 0 if columns is @settings.columns

      index = 0
      for index in [0...originals.length]
        break unless columns + @size($(originals[index])) > @settings.columns

      if index is originals.length
        index = 0
        columns = 0

      columns += @size($(originals[index]))

      # Move from originals into results
      results.push(originals.splice(index,1)[0])

    return results

$.fn.extend
  gridly: (option = {}, parameters...) ->
    @each ->
      $this = $(@)

      options = $.extend {}, $.fn.gridly.defaults, typeof option is "object" and option
      action = if typeof option is "string" then option else option.action
      action ?= "layout"

      Gridly.gridly($this, options)[action](parameters)