/*!
* Condense 0.1 - Condense and expand text heavy elements
*
* (c) 2008 Joseph Sillitoe
* Dual licensed under the MIT License (MIT-LICENSE) and GPL License,version 2 (GPL-LICENSE).
*
* Modified for vBulletin:
*  - Added callbacks when initializing condense plugin is done (onInit),
*  		when condensing is done (onCondense),
*  		when expanding is done (onExpand) and
*  		when condensing or expanding is done (onToggle).
*  - Added patch for infinite loop when condense length cutoff occurs in consecutive spaces (https://github.com/jsillitoe/jquery-condense-plugin/issues/5)
*/

/*
* jQuery plugin
*
* usage:
*
*   $(document).ready(function(){
*     $('#example1').condense();
*   });
*
* Options:
*  condensedLength: Target length of condensed element. Default: 200
*  minTrail: Minimun length of the trailing text. Default: 20
*  delim: Delimiter used for finding the break point. Default: " " - {space}
*  moreText: Text used for the more control. Default: [more]
*  lessText: Text used for the less control. Default: [less]
*  ellipsis: Text added to condensed element. Default:  ( ... )
*  moreSpeed: Animation Speed for expanding. Default: "normal"
*  lessSpeed: Animation Speed for condensing. Default: "normal"
*  easing: Easing algorith. Default: "linear"
*
*  Modified for vBulletin:
*  - Added callbacks when initializing condense plugin is done (onInit),
*  		when condensing is done (onCondense),
*  		when expanding is done (onExpand) and
*  		when condensing or expanding is done (onToggle).
*  - Added patch for infinite loop when condense length cutoff occurs in consecutive spaces (https://github.com/jsillitoe/jquery-condense-plugin/issues/5)
*/

(function($) {

  // plugin definition
  $.fn.condense = function(options) {

    $.metadata ? debug('metadata plugin detected') : debug('metadata plugin not present');//detect the metadata plugin?

    var opts = $.extend({}, $.fn.condense.defaults, options); // build main options before element iteration

    // iterate each matched element
    return this.each(function() {
	    $this = $(this);

      // support metadata plugin (v2.0)
	    var o = $.metadata ? $.extend({}, opts, $this.metadata()) : opts; // build element specific options

      debug('Condensing ['+$this.text().length+']: '+$this.text());

      var clone = cloneCondensed($this,o);

      if (clone){
        // id attribute switch.  make sure that the visible elem keeps the original id (if set).
        $this.attr('id') ? $this.attr('id','condensed_'+$this.attr('id')) : false;

        var controlMore = " <span class='condense_control condense_control_more' style='cursor:pointer;'>"+o.moreText+"</span>";
        var controlLess = " <span class='condense_control condense_control_less' style='cursor:pointer;'>"+o.lessText+"</span>";

				// Add the ellipis inside the last non block element in the truncated html so that
				// we don't have the ellispis in it's own paragraph (which can make the truncated block
				// longer than the non truncated block).
				let $truncatepoint = null,
					$lastchild = clone.contents().last();

        clone.append(controlMore);
        $this.after(clone).hide().append(controlLess);

				// We want to be careful to avoid dropping text in tags that can't handle text (such as TABLES) and
				// since any element can be displayed as a block we can't rely on tags just not being affected (such as images)
				// This should be a good trade off between good formating in the common case and not breaking things in edge cases.
				//
				// Check the tag first because that will implicitly exclude text nodes which don't allow the "css" call.
				let allowedTags = ['P', 'DIV', 'SPAN'];
				while ($lastchild && allowedTags.includes($lastchild.prop('tagName')) && $lastchild.css('display') == 'block')
				{
					$truncatepoint = $lastchild;
					$lastchild = $truncatepoint.contents().last();
				}

				let $moreControl = $('.condense_control_more', clone);

				// we have to handle things differently if we're adding directly to the element vs a contained element
				// to avoid adding after the added control.  Otherwise we could just set truncatepoint to clone.
				// (Alternatively we could add the control after the ellipsis but prefer to add it before making the
				// block visible)
				if ($truncatepoint)
				{
					$truncatepoint.append(o.ellipsis);
				}
			 	else
				{
					$moreControl.before(o.ellipsis);
				}

        $moreControl.click(function(){
          debug('moreControl clicked.');
          triggerExpand($(this),o)
        });

        $('.condense_control_less',$this).click(function(){
          debug('lessControl clicked.');
          triggerCondense($(this),o)
        });
      }
      if (typeof o.onInit == 'function'){
    		o.onInit.apply(this, [clone]);
      }
		});
  };

  function cloneCondensed(elem, opts){
    // Try to clone and condense the element.  if not possible because of the length/minTrail options, return false.
    // also, dont count tag declarations as part of the text length.
    // check the length of the text first, return false if too short.
    if ($.trim(elem.text()).length <= opts.condensedLength + opts.minTrail){
      debug('element too short: skipping.');
      return false;
    }

    let fullbody = $.trim(elem.html()),
    	fulltext = $.trim(elem.text()),
    	delim = opts.delim,
    	clone = elem.clone(),
    	delta = 0;

    do {
      // find the location of the next potential break-point.
      var loc = findDelimiterLocation(fullbody, opts.delim, (opts.condensedLength + delta));
      //set the html of the clone to the substring html of the original
// *** PATCH FOR VBV-2999 ***
//      clone.html($.trim(fullbody.substring(0,(loc+1))));
      clone.html(fullbody.substring(0,(loc+1)));
// **************************
      var cloneTextLength = clone.text().length;
      var cloneHtmlLength = clone.html().length;
      delta = clone.html().length - cloneTextLength;
      debug ("condensing... [html-length:"+cloneHtmlLength+" text-length:"+cloneTextLength+" delta: "+delta+" break-point: "+loc+"]");
    //is the length of the clone text long enough?
// *** PATCH FOR VBV-2999 ***
//    }while(clone.text().length < opts.condensedLength )
    }while(delta && clone.text().length < opts.condensedLength )
// **************************

    //  after skipping ahead to the delimiter, do we still have enough trailing text?
    if ((fulltext.length - cloneTextLength) < opts.minTrail){
      debug('not enough trailing text: skipping.');
      return false;
    }

    debug('clone condensed. [text-length:'+cloneTextLength+']');
    return clone;
  }


  function findDelimiterLocation(html, delim, startpos){
    // find the location inside the html of the delimiter, starting at the specified length.
    var foundDelim = false;
    var loc = startpos;
    do {
      var loc = html.indexOf(delim, loc);
      if (loc < 0){
        debug ("No delimiter found.");
        return html.length;
      } // if there is no delimiter found, just return the length of the entire html string.
      foundDelim = true;
      while (isInsideTag(html, loc)) {
        // if we are inside a tag, this delim doesn't count.  keep looking...
        loc++;
        foundDelim = false;
      }
    }while(!foundDelim)
    debug ("Delimiter found in html at: "+loc);
    return loc;
  }


  function isInsideTag(html, loc){
    return (html.indexOf('>',loc) < html.indexOf('<',loc));
  }


  function triggerCondense(control, opts){
    debug('Condense Trigger: '+control.html());
    var orig = control.parent(); // The original element will be the control's immediate parent.
    var condensed = orig.next(); // The condensed element will be the original immediate next sibling.
    condensed.show();
    var con_w  = condensed.width();
    var con_h = condensed.height();
    condensed.hide(); //briefly flashed the condensed element so we can get the target width/height
    var orig_w  = orig.width();
    var orig_h = orig.height();
    orig.animate({height:con_h, width:con_w, opacity: 1}, opts.lessSpeed, opts.easing,
      function(){
        orig.height(orig_h).width(orig_w).hide();
        condensed.show();
        if (typeof opts.onCondense == 'function'){
          opts.onCondense.apply(control, [condensed]);
        }
        if (typeof opts.onToggle == 'function'){
          opts.onToggle.apply(control, [orig, condensed, true]);
    	}
      });
  }


  function triggerExpand(control, opts){
    debug('Expand Trigger: '+control.html());
    var condensed = control.parent(); // The condensed element will be the control's immediate parent.
    var orig = condensed.prev(); // The original element will be the condensed immediate previous sibling.
    orig.show();
    var orig_w  = orig.width();
    var orig_h = orig.height();
    orig.width(condensed.width()+"px").height(condensed.height()+"px");
    condensed.hide();
    orig.animate({height:orig_h, width:orig_w, opacity: 1}, opts.moreSpeed, opts.easing,
      function(){
    	if (typeof opts.onExpand == 'function'){
    	  opts.onExpand.apply(control, [orig]);
    	}
    	if (typeof opts.onToggle == 'function'){
    	  opts.onToggle.apply(control, [orig, condensed, false]);
      	}
      });
    if(condensed.attr('id')){
      var idAttr = condensed.attr('id');
      condensed.attr('id','condensed_'+idAttr);
      orig.attr('id',idAttr);
    }
  }


  /**
   * private function for debugging
   */
  function debug($obj) {if (window.console && window.console.log){window.console.log($obj);}};


  // plugin defaults
  $.fn.condense.defaults = {
    condensedLength: 200,
    minTrail: 20,
    delim: " ",
    moreText: "[more]",
    lessText: "[less]",
    ellipsis: " ( ... )",
    moreSpeed: "normal",
    lessSpeed: "normal",
    easing: "linear",
    onInit: null,
    onCondense: null,
    onExpand: null,
    onToggle: null
  };

})(jQuery);
