/* ========================================================================
 * Avvo UI - truncate.js
 * ======================================================================== */

import 'dotdotdot/src/js/jquery.dotdotdot.js'
// Advanced cross-browser ellipsis for multi-line content.
// http://dotdotdot.frebsite.nl/

const $ = global.jQuery

const $window = $(window)
const $document = $(document)

let windowResizeTimeout
let windowWidth = $window.width()


// TRUNCATE PUBLIC CLASS DEFINITION
// ================================

const Truncate = function (element, options) {
  this.$element = $(element)
  this.options  = $.extend({}, Truncate.DEFAULTS, options)
  this.$toggler = $(Truncate.TOGGLE_SELECTOR).filter(`[data-target="#${element.id}"]`)

  this.$toggler.attr('aria-controls', element.id)

  this.options.expandLabel = this.options.expandLabel || this.$toggler.html()
  this.transitioning = null

  this.render()
}

Truncate.VERSION = '1.1.0'

Truncate.TRUNCATE_SELECTOR = '[data-truncate-lines]'
Truncate.TOGGLE_SELECTOR   = '[data-toggle="truncate"]'

Truncate.TRANSITION_DURATION = 350
Truncate.WINDOW_RESIZE_DELAY = 100

Truncate.DEFAULTS = {
  toleranceLines: 2,
  truncateLines:  3,
  truncateLabel:  'Less',
  expandLabel:    undefined,
}


Truncate.prototype.render = function () {
  this.calculateHeights()

  if (this.needsTruncation) {
    this.$toggler.show()

    if (this.$element.hasClass('is-truncated')) {
      this.truncate({ animate: false })
    } else {
      this.expand({ animate: false })
    }
  } else {
    this.$toggler.hide()
    this.$element.css('height', '') // Remove inline styles
  }
}


Truncate.prototype.calculateHeights = function () {
  const lineHeight = calculateLineHeight(this.$element)

  // Reset height…
  if (this.$element.hasClass('is-truncated')) {
    this.$element.trigger('destroy.dot')
  }

  this.expandedHeight  = this.$element[0].scrollHeight
  this.truncatedHeight = this.options.truncateLines * lineHeight // educated guess
  this.needsTruncation = this.truncatedHeight < (this.expandedHeight - (this.options.toleranceLines * lineHeight))
}


Truncate.prototype.truncate = function (options) {
  if (this.transitioning) {
    return
  }

  options = $.extend({ animate: true }, options)

  const startEvent = $.Event('truncate.ui.truncate')
  this.$element.trigger(startEvent)

  if (startEvent.isDefaultPrevented()) {
    return
  }

  if (options.animate) {
    // Hardcoding the element's height ensures there's a value to animate
    this.$element.height(this.$element.height())
    forceRedraw(this.$element[0])

    this.$element.addClass('truncating')
  }

  this.$element.attr('aria-expanded', false)

  this.$toggler
    .attr('aria-expanded', false)
    .trigger('blur')

  this.transitioning = true

  const complete = function () {
    this.$element
      .addClass('is-truncated')
      .dotdotdot()
      .height('')
      .removeClass('truncating')

    this.$toggler
      .html(this.options.expandLabel)

    this.transitioning = false

    this.$element.trigger('truncated.ui.truncate')
  }

  this.$element.height(this.truncatedHeight)

  if (!$.support.transition) {
    return complete.call(this)
  }

  this.$element
    .one('uiTransitionEnd', $.proxy(complete, this))
    .emulateTransitionEnd(options.animate ? Truncate.TRANSITION_DURATION : 0)
}


Truncate.prototype.expand = function (options) {
  if (this.transitioning) {
    return
  }

  options = $.extend({ animate: true }, options)

  const startEvent = $.Event('expand.ui.truncate')
  this.$element.trigger(startEvent)

  if (startEvent.isDefaultPrevented()) {
    return
  }

  if (options.animate) {
    // Update truncated height with the exact height we have currently
    // overflow:auto and outerHeight(true) ensures we factor in margins
    this.truncatedHeight = this.$element.css('overflow', 'auto').outerHeight(true)

    this.$element.height(this.truncatedHeight)
    forceRedraw(this.$element[0])

    this.$element.addClass('truncating')
  }

  this.$element
    .removeClass('is-truncated')
    .trigger('destroy.dot')

  this.$toggler
    .trigger('blur')

  this.transitioning = true

  const complete = function () {
    this.$element
      .height('')
      .removeClass('truncating')
      .attr('aria-expanded', true)

    this.$toggler
      .html(this.options.truncateLabel)
      .attr('aria-expanded', true)

    this.transitioning = false

    this.$element.trigger('expanded.ui.truncate')
  }

  this.$element.height(this.expandedHeight)

  if (!$.support.transition) {
    return complete.call(this)
  }

  this.$element
    .one('uiTransitionEnd', $.proxy(complete, this))
    .emulateTransitionEnd(options.animate ? Truncate.TRANSITION_DURATION : 0)
}


Truncate.prototype.toggle = function () {
  const action = this.$element.hasClass('is-truncated') ? 'expand' : 'truncate'
  this[action]()
}


// Normalizes browser differences in reporting line-height. Some always report it in pixels,
// others report back what was set in the stylesheet (e.g. a ratio like 1.5)
//
// @return int - The pixel line-height
function calculateLineHeight($el) {
  const rawLineHeight = $el.css('line-height')
  let lineHeight    = parseFloat(rawLineHeight)
  const isRatio       = /^\d*\.?\d*$/.test(rawLineHeight)

  if (isRatio) {
    lineHeight *= parseFloat($el.css('font-size'))
  }

  return Math.ceil(lineHeight)
}

function forceRedraw(el) {
  // Reading "offsetHeight" forces the browser to redraw the element.
  return el.offsetHeight
}

// TRUNCATE PLUGIN DEFINITION
// ==========================

function Plugin(option) {
  return this.each(function () {
    const $this = $(this)
    let data  = $this.data('ui.truncate')
    const options = $.extend({}, Truncate.DEFAULTS, $this.data(), typeof option === 'object' && option)

    // Handle special case where [data-truncate-lines] was not explicitly set
    if (options.truncateLines === '') {
      options.truncateLines = Truncate.DEFAULTS.truncateLines
    }

    // instantiate only once
    if (!data) {
      $this.data('ui.truncate', (data = new Truncate(this, options)))
    }

    // calling functions, e.g. $('.foo').truncate('toggle');
    if (typeof option === 'string') {
      data[option]()
    }
  })
}

export function init() {
  const old = $.fn.truncate

  $.fn.truncate             = Plugin
  $.fn.truncate.Constructor = Truncate


  // TRUNCATE NO CONFLICT
  // ====================

  $.fn.truncate.noConflict = function () {
    $.fn.truncate = old
    return this
  }


  // TRUNCATE DATA-API
  // =================

  // Truncate elements on load and on explicit init
  $window.on('load.ui.truncate.data-api, init.ui.truncate.data-api', () => {
    Plugin.call($(Truncate.TRUNCATE_SELECTOR))
  })

  // Truncate togglers
  $document.on('click.ui.truncate.data-api', Truncate.TOGGLE_SELECTOR, function (event) {
    event.preventDefault()
    const $toggler = $(this)
    const $target  = $($toggler.data('target'))

    Plugin.call($target, 'toggle')
  })

  $window.on('resize.ui.truncate.data-api', () => {
    clearTimeout(windowResizeTimeout)
    windowResizeTimeout = setTimeout(() => {
      if ($window.width() !== windowWidth) {
        windowWidth = $window.width()
        Plugin.call($(Truncate.TRUNCATE_SELECTOR), 'render')
      }
    }, Truncate.WINDOW_RESIZE_DELAY)
  })
}
