/**
 * EGroupware eTemplate2 - dataview code
 *
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package etemplate
 * @subpackage dataview
 * @link https://www.egroupware.org
 * @author Andreas Stöckel
 * @copyright EGroupware GmbH 2011-2021
 */

/*egw:uses
	/vendor/bower-asset/jquery/dist/jquery.js;
	et2_core_common;

	et2_dataview_model_columns;
	et2_dataview_view_grid;
	et2_dataview_view_rowProvider;
	et2_dataview_view_resizeable;
*/

import {et2_dataview_column, et2_dataview_columns} from './et2_dataview_model_columns';
import {et2_dataview_view_resizable} from "./et2_dataview_view_resizeable";
import {et2_dataview_grid} from "./et2_dataview_view_grid";
import {et2_dataview_rowProvider} from "./et2_dataview_view_rowProvider"
import {egw} from "../jsapi/egw_global";

/**
 * The et2_dataview class is the main class for displaying a dataview. The
 * dataview class manages the creation of the outer html nodes (like the table,
 * header, etc.) and contains the root container: an instance of
 * et2_dataview_view_grid, which can be accessed using the "grid" property of
 * this object.
 *
 * To better support assistive technologies (like screen readers), we now clone the visible header-row of the outer table
 * into the inner / data table and then mark the visible header-row with aria-hidden="true" to be ignored by assistive technologies:
 *
 * <table class="egwGridView_outer">
 *    <thead aria-hidden="true"><!-- contains the visible and interactive header-row --></thead>
 *    <tbody>
 *      <tr>
 *        <td colspan="all">
 *          <div class="egwGridView_scrollarea">
 *             <table class="egwGridView_grid">
 *               <thead><!-- contains the header row for assistive technologies (like screen readers) --></thead>
 *               <tbody><!-- contains the data rows for both --></tbody>
 *             </table>
 *        </td>
 *      </tr>
 *    </tbody>
 * </table>
 *
 * @augments Class
 */
export class et2_dataview
{

	/**
	 * Constant which regulates the column padding.
	 */
	columnPadding: number;

	/**
	 * Some browser dependant variables which will be calculated on creation of
	 * the first gridContainer object.
	 */
	scrollbarWidth: number;
	headerBorderWidth: number;
	columnBorderWidth: number;

	private width: number;
	private height: number;

	private uniqueId: string;

	/**
	 * Hooks to allow parent to keep up to date if things change
	 */
	onUpdateColumns: Function;
	selectColumnsClick: Function;

	private parentNode: JQuery;
	egw: any;

	private columnNodes: any[];
	private columns: any[];
	private columnMgr: et2_dataview_columns;
	rowProvider: et2_dataview_rowProvider;

	grid: et2_dataview_grid;

	// DOM stuff
	private selectColIcon: JQuery;
	private headTr: any;
	private containerTr: JQuery;
	private selectCol: JQuery;
	private thead: JQuery;
	private tbody: JQuery;
	private table: JQuery;
	private visibleColumnCount: number;


	/**
	 * Constructor for the grid container
	 *
	 * @param {DOMElement} _parentNode is the DOM-Node into which the grid view will be inserted
	 * @param {egw} _egw
	 * @memberOf et2_dataview
	 */
	constructor(_parentNode, _egw) {

		// Copy the arguments
		this.parentNode = jQuery(_parentNode);
		this.egw = _egw;

		// Initialize some variables
		this.columnNodes = []; // Array with the header containers
		this.columns = [];
		this.columnMgr = null;
		this.rowProvider = null;

		this.width = 0;
		this.height = 0;

		this.uniqueId = "gridCont_" + this.egw.uid();

		// Build the base nodes
		this._createElements();

		// Read the browser dependant variables
		this._getDepVars();
	}

	/**
	 * Destroys the object, removes all dom nodes and clears all references.
	 */
	destroy()
	{
		// Clear the columns
		this._clearHeader();

		// Free the grid
		if (this.grid)
		{
			this.grid.destroy();
			this.grid = null;
		}

		// Free the row provider
		if (this.rowProvider)
		{
			this.rowProvider.destroy();
			this.rowProvider = null;
		}

		// Detach the outer element
		this.table.remove();
		this.table = null;
	}

	/**
	 * Clears all data rows and reloads them
	 */
	clear()
	{
		if (this.grid)
		{
			this.grid.clear();
		}
	}

	/**
	 * Returns the column container node for the given column index
	 *
	 * @param _columnIdx the integer column index
	 */
	getHeaderContainerNode(_columnIdx)
	{
		if (typeof this.columnNodes[_columnIdx] != "undefined")
		{
			return this.columnNodes[_columnIdx].container[0];
		}

		return null;
	}

	/**
	 * Sets the column descriptors and creates the column header according to it.
	 * The inner grid will be emptied if it has already been built.
	 */
	setColumns(_columnData)
	{
		// Free all column objects which have been created till this moment
		this._clearHeader();

		// Copy the given column data
		this.columnMgr = new et2_dataview_columns(_columnData);

		// Create the stylesheets
		this.updateColumns();

		// Build the header row
		this._buildHeader();

		// Build the grid
		this._buildGrid();
	}

	/**
	 * Resizes the grid
	 */
	resize(_w: number, _h: number)
	{
		// Not fully initialized yet...
		if (!this.columnMgr) return;

		if (this.width != _w)
		{
			this.width = _w;

			// Take grid border width into account
			_w -= (this.table.outerWidth(true) - this.table.innerWidth());

			// Take grid header border's width into account. eg. category colors may add extra pixel into width
			_w = _w - (this.thead.find('tr').outerWidth() - this.thead.find('tr').innerWidth());

			// Rebuild the column stylesheets
			this.columnMgr.setTotalWidth(_w - this.scrollbarWidth);
			this._updateColumns();
		}

		if (this.height != _h)
		{
			this.height = _h;

			// Set the height of the grid.
			if (this.grid)
			{
				this.grid.setScrollHeight(this.height -
					this.headTr.outerHeight(true));
			}
		}
	}

	/**
	 * Returns the column manager object. You can use it to set the visibility
	 * of columns etc. Call "updateHeader" if you did any changes.
	 */
	getColumnMgr() {
		return this.columnMgr;
	}

	/**
	 * Recalculates the stylesheets which determine the column visibility and
	 * width.
	 *
	 * @param setDefault boolean Allow admins to save current settings as default for all users
	 */
	updateColumns(setDefault : boolean = false)
	{
		if (this.columnMgr)
		{
			this._updateColumns();
		}

		// Ability to notify parent / someone else
		if (this.onUpdateColumns)
		{
			this.onUpdateColumns(setDefault);
		}
	}


	/* --- PRIVATE FUNCTIONS --- */

	/* --- Code for building the grid container DOM-Tree elements ---- */

	/**
	 * Builds the base DOM-Tree elements
	 */
	private _createElements()
	{
		/*
			Structure:
			<table class="egwGridView_outer">
				<thead>
					<tr> [HEAD] </tr>
				</thead>
				<tbody>
					<tr> [GRID CONTAINER] </tr>
				</tbody>
			</table>
		*/

		this.containerTr = jQuery(document.createElement("tr"));
		this.headTr = jQuery(document.createElement("tr"));

		this.thead = jQuery(document.createElement("thead"))
			.append(this.headTr);
		this.tbody = jQuery(document.createElement("tbody"))
			.append(this.containerTr);

		this.table = jQuery(document.createElement("table"))
			.addClass("egwGridView_outer")
			.append(this.thead, this.tbody)
			.appendTo(this.parentNode);
	}


	/* --- Code for building the header row --- */

	/**
	 * Clears the header row
	 */
	private _clearHeader ()
	{
		if (this.columnMgr)
		{
			this.columnMgr.destroy();
			this.columnMgr = null;
		}

		// Remove dynamic CSS,
		for (var i = 0; i < this.columns.length; i++)
		{
			if(this.columns[i].tdClass)
			{
				this.egw.css('.'+this.columns[i].tdClass);
			}
			if(this.columns[i].divClass)
			{
				this.egw.css('.'+this.columns[i].divClass);
				this.egw.css(".egwGridView_outer ." + this.columns[i].divClass);
				this.egw.css(".egwGridView_grid ." + this.columns[i].divClass);
			}
		}
		this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow");
		this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow");
		this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow");

		// Reset the headerColumns array and empty the table row
		this.columnNodes = [];
		this.columns = [];
		this.headTr.empty();
	}

	/**
	 * Sets the column data which is retrieved by calling egwGridColumns.getColumnData.
	 * The columns will be updated.
	 */
	private _updateColumns()
	{
		// Copy the columns data
		this.columns = this.columnMgr.getColumnData();

		// Count the visible rows
		var total_cnt = 0;
		for (var i = 0; i < this.columns.length; i++)
		{
			if (this.columns[i].visible)
			{
				total_cnt++;
			}
		}

		// Set the grid column styles
		var first = true;
		var vis_col = this.visibleColumnCount = 0;
		var totalWidth = 0;
		for (var i = 0; i < this.columns.length; i++)
		{
			var col = this.columns[i];

			col.tdClass = this.uniqueId + "_td_" + col.id;
			col.divClass = this.uniqueId + "_div_" + col.id;

			if (col.visible)
			{
				vis_col++;
				this.visibleColumnCount++;

				// Update the visibility of the column
				this.egw.css("." + col.tdClass,
					"display: table-cell; " +
					"!important;");

				// Ugly browser dependant code - each browser seems to treat the
				// right (collapsed) border of the row differently
				var subBorder = 0;
				var subHBorder = 0;
				/*
				if (jQuery.browser.mozilla)
				{
					var maj = jQuery.browser.version.split(".")[0];
					if (maj < 2) {
						subBorder = 1; // Versions <= FF 3.6
					}
				}
				if (jQuery.browser.webkit)
				{
					if (!first)
					{
						subBorder = 1;
					}
					subHBorder = 1;
				}
				if ((jQuery.browser.msie || jQuery.browser.opera) && first)
				{
					subBorder = -1;
				}
				*/

				// Make the last columns one pixel smaller, to prevent a horizontal
				// scrollbar from showing up
				if (vis_col == total_cnt)
				{
					subBorder += 1;
				}

				// Write the width of the header columns
				//Mobile can not resize the header columns anyway. Just fit them to the content

				if (window.egwIsMobile())
				{
					this.egw.css(".egwGridView_outer ." + col.tdClass, "width: " + col.width + "px;");
					this.egw.css(".egwGridView_outer ." + col.divClass, "width: fit-content;")
				}else
				{
					const headerWidth = Math.max(0, (col.width - this.headerBorderWidth - subHBorder));
					this.egw.css(".egwGridView_outer ." + col.divClass, "width: " + headerWidth + "px;");

				// Write the width of the body-columns
					const columnWidth = Math.max(0, (col.width - this.columnBorderWidth - subBorder));
				this.egw.css(".egwGridView_grid ." + col.divClass,
					"width: " + columnWidth + "px;");

				totalWidth += col.width;

				first = false;
				}
			}
			else
			{
				this.egw.css("." + col.tdClass, "display: none;");
			}
		}

		// Add the full row and spacer class
		this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow",
			"width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;");
		this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow",
			"border-right-width: 0 !important;");
		this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow",
			"width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;");
	}

	/**
	 * Builds the containers for the header row
	 */
	private _buildHeader()
	{
		var self = this;
		var handler = function(event) {
		};
		for (var i = 0; i < this.columns.length; i++)
		{
			var col = this.columns[i];

			// Create the column header and the container element
			var cont = jQuery(document.createElement("div"))
				.addClass("innerContainer")
				.addClass(col.divClass);

			var column = jQuery(document.createElement("th"))
				.addClass(col.tdClass)
				.attr("align", "left")
				.append(cont)
				.appendTo(this.headTr);

			if(this.columnMgr && this.columnMgr.getColumnById(i))
			{
				column.addClass(this.columnMgr.getColumnById(i).fixedWidth ? 'fixedWidth' : 'relativeWidth');
				if(this.columnMgr.getColumnById(i).visibility === et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT)
				{
					column.addClass('noResize');
				}
			}

			// make column resizable
			var enc_column = self.columnMgr.getColumnById(col.id);
			if(enc_column.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT)
			{
				et2_dataview_view_resizable.makeResizeable(column, function(_w) {

					// User wants the column to stay where they put it, even for relative
					// width columns, so set it explicitly first and adjust other relative
					// columns to match.
					if(this.relativeWidth)
					{
						// Set to selected width
						this.set_width(_w + "px");
						self.columnMgr.updated();
						// Just triggers recalculation
						self.columnMgr.getColumnWidth(0);

						// Set relative widths to match
						var relative = self.columnMgr.totalWidth - self.columnMgr.totalFixed + _w;
						this.set_width(_w / relative);
						for(var i = 0; i < self.columnMgr.columnCount(); i++)
						{
							var col = self.columnMgr.getColumnById('col_'+i);
							if(!col || col == this || col.fixedWidth) continue;
							col.set_width(self.columnMgr.getColumnWidth(i) / relative);
						}
						// Triggers column change callback, which saves
						self.updateColumns();
					}
					else
					{
						this.set_width(this.relativeWidth ? (_w / self.columnMgr.totalWidth) : _w + "px");
						self.columnMgr.updated();
						self.updateColumns();
					}

				}, enc_column);
			}

			// Store both nodes in the columnNodes array
			this.columnNodes.push({
				"column": column,
				"container": cont
			});
		}

		this._buildSelectCol();
	}

	/**
	 * Builds the select cols column
	 */
	private _buildSelectCol()
	{
		// Build the "select columns" icon
		this.selectColIcon = jQuery(document.createElement("span"))
			.addClass("selectcols")
			.css('display', 'inline-block');	// otherwise jQuery('span.selectcols',this.dataview.headTr).show() set it to "inline" causing it to not show up because 0 height

		// Build the option column
		this.selectCol = jQuery(document.createElement("th"))
			.addClass("optcol")
			.append(this.selectColIcon)
			// Toggle display of option popup
			.click(this, function(e) {if(e.data.selectColumnsClick) e.data.selectColumnsClick(e);})
			.appendTo(this.headTr);

		this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth()
				+ this.selectCol.width() + 1);
	}

	/**
	 * Builds the inner grid class
	 */
	private _buildGrid()
	{
		// Create the collection of column ids
		var colIds = [];
		for (var i = 0; i < this.columns.length; i++)
		{
			if(this.columns[i].visible)
			{
				colIds[i] = this.columns[i].id;
			}
		}

		// Create the row provider
		if(this.rowProvider)
		{
			this.rowProvider.destroy();
		}

		this.rowProvider = new et2_dataview_rowProvider(this.uniqueId, colIds);

		// Create the grid class and pass "19" as the starting average row height
		this.grid = new et2_dataview_grid(null, null, this.egw, this.rowProvider, 19);

		// Copy view header into grid for accessibility
		this.grid.innerTbody.before(this.thead.clone());

		// Insert the grid into the DOM-Tree
		var tr = jQuery(this.grid.getFirstNode());
		this.containerTr.replaceWith(tr);
		this.containerTr = tr;

		// Hide the outer thead from assistive technologies (like screen readers)
		this.thead.attr({"aria-hidden": true});
	}

	/* --- Code for calculating the browser/css depending widths --- */

	/**
	 * Reads the browser dependant variables
	 */
	private _getDepVars()
	{
		if (typeof this.scrollbarWidth === 'undefined')
		{
			// Clone the table and attach it to the outer body tag
			var clone = this.table.clone();
			jQuery(egw.top.document.getElementsByTagName("body")[0])
				.append(clone);

			// Read the scrollbar width
			this.scrollbarWidth = this.constructor.prototype.scrollbarWidth =
				this._getScrollbarWidth(clone);

			// Read the header border width
			this.headerBorderWidth = this.constructor.prototype.headerBorderWidth =
				this._getHeaderBorderWidth(clone);

			// Read the column border width
			this.columnBorderWidth = this.constructor.prototype.columnBorderWidth =
				this._getColumnBorderWidth(clone);

			// Remove the cloned DOM-Node again from the outer body
			clone.remove();
		}
	}

	/**
	 * Reads the scrollbar width
	 */
	private _getScrollbarWidth(_table: JQuery)
	{
		// Create a temporary td and two divs, which are inserted into the
		// DOM-Tree. The outer div has a fixed size and "overflow" set to auto.
		// When the second div is inserted, it will be forced to display a scrollbar.
		var div_inner = jQuery(document.createElement("div"))
			.css("height", "1000px");
		var div_outer = jQuery(document.createElement("div"))
			.css("height", "100px")
			.css("width", "100px")
			.css("overflow", "auto")
			.append(div_inner);
		var td = jQuery(document.createElement("td"))
			.append(div_outer);

		// Store the scrollbar width statically.
		jQuery("tbody tr", _table).append(td);
		var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth());

		// Remove the elements again
		div_outer.remove();

		return width;
	}

	/**
	 * Calculates the total width of the header column border
	 */
	private _getHeaderBorderWidth(_table: JQuery)
	{
		// Create a temporary th which is appended to the outer thead row
		var cont = jQuery(document.createElement("div"))
			.addClass("innerContainer");

		var th = jQuery(document.createElement("th"))
			.append(cont);

		// Insert the th into the document tree
		jQuery("thead tr", _table).append(th);

		// Calculate the total border width
		var width = th.outerWidth(true) - cont.width();

		// Remove the appended element again
		th.remove();

		return width;
	}

	/**
	 * Calculates the total width of the column border
	 */
	private _getColumnBorderWidth(_table: JQuery)
	{
		// Create a temporary th which is appended to the outer thead row
		var cont = jQuery(document.createElement("div"))
			.addClass("innerContainer");

		var td = jQuery(document.createElement("td"))
			.append(cont);

		// Insert the th into the document tree
		jQuery("tbody tr", _table).append(td);

		// Calculate the total border width
		_table.addClass("egwGridView_grid");
		var width = td.outerWidth(true) - cont.width();

		// Remove the appended element again
		td.remove();

		return width;
	}
}