/* --------------------------------------------------------------
 datatable_responsive_columns.js 2017-03-08
 Gambio GmbH
 http://www.gambio.de
 Copyright (c) 2017 Gambio GmbH
 Released under the GNU General Public License (Version 2)
 [http://www.gnu.org/licenses/gpl-2.0.html]
 --------------------------------------------------------------
 */

/**
 * ## Enable DataTable Responsive Columns
 *
 * This module will enable the responsive columns functionality which will resize the columns until a minimum
 * width is reach. Afterwards the columns will be hidden and the content will be displayed by through an icon
 * tooltip.
 *
 * ### Options
 *
 * **Initial Visibility Toggle Selector | `data-data_relative_columns-visibility-toggle-selector` | String | Optional**
 *
 * Provide a selector relative to each thead > tr element in order to hide the column on page load and then show it
 * again once the responsive widths have been calculated. The provided selector must point to the biggest column in
 * order to avoid broken displays till the table becomes responsive.
 *
 * @module Admin/Extensions/data_relative_columns
 */
gx.extensions.module(
	'datatable_responsive_columns',
	
	[
		`${jse.source}/vendor/qtip2/jquery.qtip.min.css`,
		`${jse.source}/vendor/qtip2/jquery.qtip.min.js`,
	],
	
	function(data) {
		
		'use strict';
		
		// ------------------------------------------------------------------------
		// VARIABLES
		// ------------------------------------------------------------------------
		
		/**
		 * Module Selector
		 *
		 * @type {jQuery}
		 */
		const $this = $(this);
		
		/**
		 * Default Options
		 *
		 * @type {Object}
		 */
		const defaults = {
			visibilityToggleSelector: '[data-column-name="actions"]'
		};
		
		/**
		 * Final Options
		 *
		 * @type {Object}
		 */
		const options = $.extend(true, {}, defaults, data);
		
		/**
		 * Module Instance
		 *
		 * @type {Object}
		 */
		const module = {};
		
		/**
		 * DataTable Initialization Columns
		 *
		 * @type {Array}
		 */
		let columnDefinitions;
		
		/**
		 * Width Factor Sum
		 *
		 * @type {Number}
		 */
		let widthFactorSum;
		
		// ------------------------------------------------------------------------
		// FUNCTIONS
		// ------------------------------------------------------------------------
		
		/**
		 * Update empty table "colspan" attribute.
		 *
		 * This method will keep the empty table row width in sync with the table width.
		 */
		function _updateEmptyTableColSpan() {
			if ($this.find('.dataTables_empty').length > 0) {
				const colspan = ($this.find('thead:first tr:first .actions').index() + 1)
					- $this.find('thead:first tr:first th.hidden').length;
				$this.find('.dataTables_empty').attr('colspan', colspan);
			}
		}
		
		/**
		 * Add hidden columns content icon to actions cell of a single row.
		 *
		 * Call this method only if you are sure there is no icon previously set (runs faster).
		 *
		 * @param {jQuery} $tr
		 */
		function _addHiddenColumnsContentIcon($tr) {
			$tr.find('td.actions div:first')
				.prepend(`<i class="fa fa-ellipsis-h meta-icon hidden-columns-content"></i>`);
		}
		
		/**
		 * Get the cell content.
		 *
		 * This method will also search for child input and select elements and return the appropriate content.
		 *
		 * @param {jQuery} $td Table cell to be examined.
		 *
		 * @return {String}
		 */
		function _getCellContent($td) {
			if ($td.find('select').length) {
				return $td.find('select option:selected').text();
			} else if ($td.find('input:text').length) {
				return $td.find('input:text').val();
			} else if ($td.find('input:checkbox').length) {
				return $td.find('input:checkbox').prop('checked') ? '✔' : '✘';
			} else {
				return $td.text();
			}
		}
		
		/**
		 * Generates and sets the tooltip content for the hidden columns content.
		 *
		 * @param {jQuery} $tr The current row selector.
		 */
		function _generateHiddenColumnsContent($tr) {
			let hiddenColumnContentHtml = '';
			
			$tr.find('td.hidden').each((index, td) => {
				hiddenColumnContentHtml += $this.find(`thead:first tr:first th:eq(${$(td).index()})`).text()
					+ ': ' + _getCellContent($(td)) + '<br/>';
			});
			
			$tr.find('.hidden-columns-content').qtip({
				content: hiddenColumnContentHtml,
				style: {
					classes: 'gx-qtip info'
				},
				hide: {
					fixed: true,
					delay: 300
				}
			});
		}
		
		/**
		 * Hide DataTable Columns
		 *
		 * This method is part of the responsive tables solution.
		 *
		 * @param {jQuery} $targetWrapper Target datatable instance wrapper div.
		 * @param {jQuery} $firstHiddenColumn The first hidden column (first column with the .hidden class).
		 */
		function _hideColumns($targetWrapper, $firstHiddenColumn) {
			const $lastVisibleColumn = ($firstHiddenColumn.length !== 0)
				? $firstHiddenColumn.prev()
				: $this.find('thead:first th.actions').prev();
			
			if ($lastVisibleColumn.hasClass('hidden') || $lastVisibleColumn.index() === 0) {
				return; // First column or already hidden, do not continue.
			}
			
			// Show hidden column content icon.
			if ($this.find('.hidden-columns-content').length === 0) {
				$this.find('tbody tr').each((index, tr) => {
					_addHiddenColumnsContentIcon($(tr));
				});
			}
			
			// Hide the last visible column.
			$this.find('tr').each((index, tr) => {
				$(tr)
					.find(`th:eq(${$lastVisibleColumn.index()}), td:eq(${$lastVisibleColumn.index()})`)
					.addClass('hidden');
				
				// Generate the hidden columns content.
				_generateHiddenColumnsContent($(tr));
			});
			
			_updateEmptyTableColSpan();
			
			// If there are still columns which don't fit within the viewport, hide them.
			if ($targetWrapper.width() < $this.width() && $lastVisibleColumn.index() > 1) {
				_toggleColumnsVisibility();
			}
		}
		
		/**
		 * Show DataTable Columns
		 *
		 * This method is part of the responsive tables solution.
		 *
		 * @param {jQuery} $targetWrapper Target datatable instance wrapper div.
		 * @param {jQuery} $firstHiddenColumn The first hidden column (first column with the .hidden class).
		 */
		function _showColumns($targetWrapper, $firstHiddenColumn) {
			if ($firstHiddenColumn.length === 0) {
				return;
			}
			
			const firstHiddenColumnWidth = parseInt($firstHiddenColumn.css('min-width'));
			let tableMinWidth = 0;
			
			// Calculate the table min width by each column min width.
			$this.find('thead:first tr:first th').each((index, th) => {
				if (!$(th).hasClass('hidden')) {
					tableMinWidth += parseInt($(th).css('min-width'));
				}
			});
			
			// Show the first hidden column.
			if (tableMinWidth + firstHiddenColumnWidth <= $targetWrapper.outerWidth()) {
				$this.find('tr').each((index, tr) => {
					$(tr)
						.find(`th:eq(${$firstHiddenColumn.index()}), td:eq(${$firstHiddenColumn.index()})`)
						.removeClass('hidden');
					
					_generateHiddenColumnsContent($(tr));
				});
				
				_updateEmptyTableColSpan();
				
				// Hide hidden column content icon.
				if ($this.find('thead:first tr:first th.hidden').length === 0) {
					$this.find('.hidden-columns-content').remove();
				}
				
				// If there are still columns which would fit fit within the viewport, show them.
				const newTableMinWidth = tableMinWidth + firstHiddenColumnWidth
					+ parseInt($firstHiddenColumn.next('.hidden').css('min-width'));
				
				if (newTableMinWidth <= $targetWrapper.outerWidth() && $firstHiddenColumn.next('.hidden').length
					!== 0) {
					_toggleColumnsVisibility();
				}
			}
		}
		
		/**
		 * Toggle column visibility depending the window size.
		 */
		function _toggleColumnsVisibility() {
			const $targetWrapper = $this.parent();
			const $firstHiddenColumn = $this.find('thead:first th.hidden:first');
			
			if ($targetWrapper.width() < $this.width()) {
				_hideColumns($targetWrapper, $firstHiddenColumn);
			} else {
				_showColumns($targetWrapper, $firstHiddenColumn);
			}
		}
		
		/**
		 * Calculate and set the relative column widths.
		 *
		 * The relative width calculation works with a width-factor system where each column preserves a
		 * specific amount of the table width.
		 *
		 * This factor is not defining a percentage, rather only a width-volume. Percentage widths will not
		 * work correctly when the table has fewer columns than the original settings.
		 */
		function _applyRelativeColumnWidths() {
			$this.find('thead:first tr:first th').each(function() {
				if ($(this).css('display') === 'none') {
					return true;
				}
				
				let currentColumnDefinition;
				
				columnDefinitions.forEach((columnDefinition) => {
					if (columnDefinition.name === $(this).data('columnName')) {
						currentColumnDefinition = columnDefinition;
					}
				});
				
				if (currentColumnDefinition && currentColumnDefinition.widthFactor) {
					const index = $(this).index();
					const width = Math.round(currentColumnDefinition.widthFactor / widthFactorSum * 100 * 100) / 100;
					$this.find('thead').each((i, thead) => {
						$(thead).find('tr').each((i, tr) => {
							$(tr).find('th').eq(index).css('width', width + '%');
						});
					});
				}
			});
		}
		
		/**
		 * Applies the column width if the current column width is smaller.
		 */
		function _applyMinimumColumnWidths() {
			$this.find('thead:first tr:first th').each(function(index) {
				if ($(this).css('display') === 'none') {
					return true;
				}
				
				let currentColumnDefinition;
				
				columnDefinitions.forEach((columnDefinition) => {
					if (columnDefinition.name === $(this).data('columnName')) {
						currentColumnDefinition = columnDefinition;
					}
				});
				
				if (!currentColumnDefinition) {
					return true;
				}
				
				const currentWidth = $(this).outerWidth();
				const definitionMinWidth = parseInt(currentColumnDefinition.minWidth);
				
				if (currentWidth < definitionMinWidth) {
					// Force the correct column min-widths for all thead columns.
					$this.find('thead').each((i, thead) => {
						$(thead).find('tr').each((i, tr) => {
							$(tr).find('th').eq(index)
								.outerWidth(definitionMinWidth)
								.css('max-width', definitionMinWidth)
								.css('min-width', definitionMinWidth);
						});
					});
				}
			});
		}
		
		/**
		 * On DataTable Draw Event
		 */
		function _onDataTableDraw() {
			// Wait until the contents of the table are rendered. DataTables will sometimes fire the draw event
			// even before the td elements are rendered in the browser.
			var interval = setInterval(function () {
				if ($this.find('tbody tr:last td.actions').length === 1) {
					_applyRelativeColumnWidths();
					_applyMinimumColumnWidths();
					_toggleColumnsVisibility();
					
					// Hide the tbody cells depending on whether the respective <th> element is hidden.
					$this.find('thead:first tr:first th').each(function (index, th) {
						if ($(th).hasClass('hidden')) {
							$this.find('tbody tr').each(function (i, tr) {
								$(tr).find('td:eq(' + index + ')').addClass('hidden');
							});
						}
					});
					
					// Add the hidden columns icon if needed.
					if ($this.find('thead th.hidden').length) {
						$this.find('tbody tr').each(function (index, tr) {
							if ($(tr).find('.hidden-columns-content').length) {
								return true;
							}
							_addHiddenColumnsContentIcon($(tr));
							_generateHiddenColumnsContent($(tr));
						});
					}
					
					clearInterval(interval);
				}
			}, 500);
		}
		
		/**
		 * On Window Resize Event
		 */
		function _onWindowResize() {
			$this.find('thead.fixed').outerWidth($this.outerWidth());
			_applyRelativeColumnWidths();
			_applyMinimumColumnWidths();
			_toggleColumnsVisibility();
		}
		
		/**
		 * On DataTable Initialize Event 
		 */
		function _onDataTableInit() {
			$this.find(options.visibilityToggleSelector).show();
			_updateEmptyTableColSpan();
			
			columnDefinitions = $this.DataTable().init().columns;
			widthFactorSum = 0;
			
			columnDefinitions.forEach((columnDefinition) => {
				widthFactorSum += columnDefinition.widthFactor || 0;
			});
			
			$this.on('draw.dt', _onDataTableDraw);
			$(window).on('resize', _onWindowResize);
			
			_onWindowResize();
		}
		
		// ------------------------------------------------------------------------
		// INITIALIZATION
		// ------------------------------------------------------------------------
		
		module.init = function(done) {
			$this.on('init.dt', _onDataTableInit);
			
			$(window).on('JSENGINE_INIT_FINISHED', () => {
				if ($this.DataTable().ajax.json() !== undefined) {
					_onDataTableInit();
				}
			});
			
			$this.find(options.visibilityToggleSelector).hide();
			
			done();
		};
		
		return module;
		
	});