text: import {Feature} from '../../feature';
  import {isUndef, isObj, EMPTY_FN} from '../../types';
  import {createElm, elm, tag} from '../../dom';
  import {addEvt, bound} from '../../event';
  import {parse as parseNb} from '../../number';
  import {
      NONE, CELL_TAG, HEADER_TAG, STRING, NUMBER, DATE, FORMATTED_NUMBER,
      IP_ADDRESS
  } from '../../const';
  import {defaultsStr, defaultsFn, defaultsArr} from '../../settings';
  
  /**
   * SortableTable Adapter module
   */
  export default class AdapterSortableTable extends Feature {
  
      /**
       * Creates an instance of AdapterSortableTable
       * @param {TableFilter} tf TableFilter instance
       * @param {Object} opts Configuration object
       */
      constructor(tf, opts) {
          super(tf, AdapterSortableTable);
  
          /**
           * Module name
           * @type {String}
           */
          this.name = opts.name;
  
          /**
           * Module description
           * @type {String}
           */
          this.desc = defaultsStr(opts.description, 'Sortable table');
  
          /**
           * Indicate whether table previously sorted
           * @type {Boolean}
           * @private
           */
          this.sorted = false;
  
          /**
           * List of sort type per column basis
           * @type {Array}
           */
          this.sortTypes = defaultsArr(opts.types, tf.colTypes);
  
          /**
           * Column to be sorted at initialization, ie:
           * sort_col_at_start: [1, true]
           * @type {Array}
           */
          this.sortColAtStart = defaultsArr(opts.sort_col_at_start, null);
  
          /**
           * Enable asynchronous sort, if triggers are external
           * @type {Boolean}
           */
          this.asyncSort = Boolean(opts.async_sort);
  
          /**
           * List of element IDs triggering sort on a per column basis
           * @type {Array}
           */
          this.triggerIds = defaultsArr(opts.trigger_ids, []);
  
          // edit .sort-arrow.descending / .sort-arrow.ascending in
          // tablefilter.css to reflect any path change
          /**
           * Path to images
           * @type {String}
           */
          this.imgPath = defaultsStr(opts.images_path, tf.themesPath);
  
          /**
           * Blank image file name
           * @type {String}
           */
          this.imgBlank = defaultsStr(opts.image_blank, '');
  
          /**
           * Css class for sort indicator image
           * @type {String}
           */
          this.imgClassName = defaultsStr(opts.image_class_name, 'fa solid fa-sort');
  
          /**
           * Css class for ascending sort indicator image
           * @type {String}
           */
          this.imgAscClassName = defaultsStr(opts.image_asc_class_name,
              '-up');
  
          /**
           * Css class for descending sort indicator image
           * @type {String}
           */
          this.imgDescClassName = defaultsStr(opts.image_desc_class_name,
              '-down');
  
          /**
           * Cell attribute key storing custom value used for sorting
           * @type {String}
           */
          this.customKey = defaultsStr(opts.custom_key, 'data-tf-sortKey');
  
          /**
           * Callback fired when sort extension is instanciated
           * @type {Function}
           */
          this.onSortLoaded = defaultsFn(opts.on_sort_loaded, EMPTY_FN);
  
          /**
           * Callback fired before a table column is sorted
           * @type {Function}
           */
          this.onBeforeSort = defaultsFn(opts.on_before_sort, EMPTY_FN);
  
          /**
           * Callback fired after a table column is sorted
           * @type {Function}
           */
          this.onAfterSort = defaultsFn(opts.on_after_sort, EMPTY_FN);
  
          /**
           * SortableTable instance
           * @private
           */
          this.stt = null;
  
          this.enable();
      }
  
      /**
       * Initializes AdapterSortableTable instance
       */
      init() {
          if (this.initialized) {
              return;
          }
          let tf = this.tf;
          let adpt = this;
  
          // SortableTable class sanity check (sortabletable.js)
          if (isUndef(SortableTable)) {
              throw new Error('SortableTable class not found.');
          }
  
          // Add any date format if needed
          this.emitter.emit('add-date-type-formats', this.tf, this.sortTypes);
  
          this.overrideSortableTable();
          this.setSortTypes();
  
          this.onSortLoaded(tf, this);
  
          /*** SortableTable callbacks ***/
          this.stt.onbeforesort = function () {
              adpt.onBeforeSort(tf, adpt.stt.sortColumn);
  
              /*** sort behaviour for paging ***/
              if (tf.paging) {
                  tf.feature('paging').disable();
              }
          };
  
          this.stt.onsort = function () {
              adpt.sorted = true;
  
              //sort behaviour for paging
              if (tf.paging) {
                  let paginator = tf.feature('paging');
                  // recalculate valid rows index as sorting may have change it
                  tf.getValidRows(true);
                  paginator.enable();
                  paginator.setPage(paginator.getPage());
              }
  
              adpt.onAfterSort(tf, adpt.stt.sortColumn, adpt.stt.descending);
              adpt.emitter.emit('column-sorted', tf, adpt.stt.sortColumn,
                  adpt.stt.descending);
          };
  
          // Column sort at start
          let sortColAtStart = adpt.sortColAtStart;
          if (sortColAtStart) {
              this.stt.sort(sortColAtStart[0], sortColAtStart[1]);
          }
  
          this.emitter.on(['sort'], bound(this.sortByColumnIndexHandler, this));
  
          /** @inherited */
          this.initialized = true;
  
          this.emitter.emit('sort-initialized', tf, this);
      }
  
      /**
       * Sort specified column
       * @param {Number} colIdx Column index
       * @param {Boolean} desc Optional: descending manner
       */
      sortByColumnIndex(colIdx, desc) {
          this.stt.sort(colIdx, desc);
      }
  
      /** @private */
      sortByColumnIndexHandler(tf, colIdx, desc) {
          this.sortByColumnIndex(colIdx, desc);
      }
  
      /**
       * Set SortableTable overrides for TableFilter integration
       */
      overrideSortableTable() {
          let adpt = this,
              tf = this.tf;
  
          /**
           * Overrides headerOnclick method in order to handle th event
           * @param  {Object} e [description]
           */
          SortableTable.prototype.headerOnclick = function (evt) {
              if (!adpt.initialized) {
                  return;
              }
  
              // find Header element
              let el = evt.target || evt.srcElement;
  
              while (el.tagName !== CELL_TAG && el.tagName !== HEADER_TAG) {
                  el = el.parentNode;
              }
  
              this.sort(
                  SortableTable.msie ?
                      SortableTable.getCellIndex(el) : el.cellIndex
              );
          };
  
          /**
           * Overrides getCellIndex IE returns wrong cellIndex when columns are
           * hidden
           * @param  {Object} oTd TD element
           * @return {Number}     Cell index
           */
          SortableTable.getCellIndex = function (oTd) {
              let cells = oTd.parentNode.cells,
                  l = cells.length, i;
              for (i = 0; cells[i] !== oTd && i < l; i++) { }
              return i;
          };
  
          /**
           * Overrides initHeader in order to handle filters row position
           * @param  {Array} oSortTypes
           */
          SortableTable.prototype.initHeader = function (oSortTypes) {
              let stt = this;
              if (!stt.tHead) {
                  if (tf.gridLayout) {
                      stt.tHead = tf.feature('gridLayout').headTbl.tHead;
                  } else {
                      return;
                  }
              }
  
              stt.headersRow = tf.headersRow;
              let cells = stt.tHead.rows[stt.headersRow].cells;
              stt.sortTypes = oSortTypes || [];
              let l = cells.length;
              let img, c;
  
              for (let i = 0; i < l; i++) {
                  c = cells[i];
                  if (stt.sortTypes[i] !== null && stt.sortTypes[i] !== 'None') {
                      c.style.cursor = 'pointer';
                      img = createElm('i',
                          ['class', adpt.imgBlank]);
                      c.appendChild(img);
                      if (stt.sortTypes[i] !== null) {
                          c.setAttribute('_sortType', stt.sortTypes[i]);
                      }
                      addEvt(c, 'click', stt._headerOnclick);
                  } else {
                      c.setAttribute('_sortType', oSortTypes[i]);
                      c._sortType = 'None';
                  }
              }
              stt.updateHeaderArrows();
          };
  
          /**
           * Overrides updateHeaderArrows in order to handle arrows indicators
           */
          SortableTable.prototype.updateHeaderArrows = function () {
              let stt = this;
              let cells, l, img;
  
              // external headers
              if (adpt.asyncSort && adpt.triggerIds.length > 0) {
                  let triggers = adpt.triggerIds;
                  cells = [];
                  l = triggers.length;
                  for (let j = 0; j < l; j++) {
                      cells.push(elm(triggers[j]));
                  }
              } else {
                  if (!this.tHead) {
                      return;
                  }
                  cells = stt.tHead.rows[stt.headersRow].cells;
                  l = cells.length;
              }
              for (let i = 0; i < l; i++) {
                  let cell = cells[i];
                  if (!cell) {
                      continue;
                  }
                  let cellAttr = cell.getAttribute('_sortType');
                  if (cellAttr !== null && cellAttr !== 'None') {
                      img = cell.lastChild || cell;
                      if (img.nodeName.toLowerCase() !== 'i') {
                          img = createElm('i',
                              ['class', adpt.imgBlank]);
                          cell.appendChild(img);
                      }
                      if (i === stt.sortColumn) {
                          img.className = adpt.imgClassName + 
                              (this.descending ?
                                  adpt.imgDescClassName :
                                  adpt.imgAscClassName);
                      } else {
                          img.className = adpt.imgClassName;
                      }
                  }
              }
          };
  
          /**
           * Overrides getRowValue for custom key value feature
           * @param  {Object} oRow    Row element
           * @param  {String} sType
           * @param  {Number} nColumn
           * @return {String}
           */
          SortableTable.prototype.getRowValue = function (oRow, sType, nColumn) {
              let stt = this;
              // if we have defined a custom getRowValue use that
              let sortTypeInfo = stt._sortTypeInfo[sType];
              if (sortTypeInfo && sortTypeInfo.getRowValue) {
                  return sortTypeInfo.getRowValue(oRow, nColumn);
              }
              let c = oRow.cells[nColumn];
              let s = SortableTable.getInnerText(c);
              return stt.getValueFromString(s, sType);
          };
  
          /**
           * Overrides getInnerText in order to avoid Firefox unexpected sorting
           * behaviour with untrimmed text elements
           * @param  {Object} cell DOM element
           * @return {String}       DOM element inner text
           */
          SortableTable.getInnerText = function (cell) {
              if (!cell) {
                  return;
              }
              if (cell.getAttribute(adpt.customKey)) {
                  return cell.getAttribute(adpt.customKey);
              } else {
                  return tf.getCellValue(cell);
              }
          };
      }
  
      /**
       * Adds a sort type
       */
      addSortType(...args) {
          // Extract the arguments
          let [id, caster, sorter, getRowValue] = args;
          SortableTable.prototype.addSortType(id, caster, sorter, getRowValue);
      }
  
      /**
       * Sets the sort types on a column basis
       * @private
       */
      setSortTypes() {
          let tf = this.tf,
              sortTypes = this.sortTypes,
              _sortTypes = [];
  
          tf.eachCol((i) => {
              let colType;
              if (sortTypes[i]) {
                  colType = sortTypes[i];
                  if (isObj(colType)) {
                      if (colType.type === DATE) {
                          colType = this._addDateType(i, sortTypes);
                      }
                      else if (colType.type === FORMATTED_NUMBER) {
                          let decimal = colType.decimal || tf.decimalSeparator;
                          colType = this._addNumberType(i, decimal);
                      }
                  } else {
                      colType = colType.toLowerCase();
                      if (colType === DATE) {
                          colType = this._addDateType(i, sortTypes);
                      }
                      else if (colType === FORMATTED_NUMBER ||
                          colType === NUMBER) {
                          colType = this._addNumberType(i, tf.decimalSeparator);
                      }
                      else if (colType === NONE) {
                          // TODO: normalise 'none' vs 'None'
                          colType = 'None';
                      }
                  }
              } else {
                  colType = STRING;
              }
              _sortTypes.push(colType);
          });
  
          //Public TF method to add sort type
  
          //Custom sort types
          this.addSortType('caseinsensitivestring', SortableTable.toUpperCase);
          this.addSortType(STRING);
          this.addSortType(IP_ADDRESS, ipAddress, sortIP);
  
          this.stt = new SortableTable(tf.dom(), _sortTypes);
  
          /*** external table headers adapter ***/
          if (this.asyncSort && this.triggerIds.length > 0) {
              let triggers = this.triggerIds;
              for (let j = 0; j < triggers.length; j++) {
                  if (triggers[j] === null) {
                      continue;
                  }
                  let trigger = elm(triggers[j]);
                  if (trigger) {
                      trigger.style.cursor = 'pointer';
  
                      addEvt(trigger, 'click', (evt) => {
                          let elm = evt.target;
                          if (!this.tf.sort) {
                              return;
                          }
                          this.stt.asyncSort(triggers.indexOf(elm.id));
                      });
                      trigger.setAttribute('_sortType', _sortTypes[j]);
                  }
              }
          }
      }
  
      _addDateType(colIndex, types) {
          let tf = this.tf;
          let dateType = tf.feature('dateType');
          let locale = dateType.getOptions(colIndex, types).locale || tf.locale;
          let colType = `${DATE}-${locale}`;
  
          this.addSortType(colType, (value) => {
              let parsedDate = dateType.parse(value, locale);
              // Invalid date defaults to Wed Feb 04 -768 11:00:00
              return isNaN(+parsedDate) ? new Date(-86400000000000) : parsedDate;
          });
          return colType;
      }
  
      _addNumberType(colIndex, decimal) {
          let colType = `${FORMATTED_NUMBER}${decimal === '.' ? '' : '-custom'}`;
  
          this.addSortType(colType, (value) => {
              return parseNb(value, decimal);
          });
          return colType;
      }
  
      /**
       * Remove extension
       */
      destroy() {
          if (!this.initialized) {
              return;
          }
          let tf = this.tf;
          this.emitter.off(['sort'], bound(this.sortByColumnIndexHandler, this));
          this.sorted = false;
          this.stt.destroy();
  
          let ids = tf.getFiltersId();
          for (let idx = 0; idx < ids.length; idx++) {
              let header = tf.getHeaderElement(idx);
              let img = tag(header, 'i');
  
              if (img.length === 1) {
                  header.removeChild(img[0]);
              }
          }
          this.initialized = false;
      }
  
  }
  
  AdapterSortableTable.meta = {altName: 'sort'};
  
  //Converters
  function ipAddress(value) {
      let vals = value.split('.');
      // eslint-disable-next-line no-unused-vars
      for (let x in vals) {
          let val = vals[x];
          while (3 > val.length) {
              val = '0' + val;
          }
          vals[x] = val;
      }
      return vals.join('.');
  }
  
  function sortIP(a, b) {
      let aa = ipAddress(a.value.toLowerCase());
      let bb = ipAddress(b.value.toLowerCase());
      if (aa === bb) {
          return 0;
      } else if (aa < bb) {
          return -1;
      } else {
          return 1;
      }
  }
  