import { Utility } from '../../core/utility';
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';

import './hide-columns.sass';

import { TableIndices } from '../../lib/table/table';

const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns';
const TABLE_HEADER_IDENT = 'uw-hide-column-header';
const HIDE_COLUMNS_HIDER_LABEL = 'uw-hide-columns--hider-label';
const HIDE_COLUMNS_NO_HIDE = 'uw-hide-columns--no-hide';
const HIDE_COLUMNS_DEFAULT_HIDDEN = 'uw-hide-column-default-hidden';

const TABLE_UTILS_ATTR = 'table-utils';
const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`;

const TABLE_HIDER_CLASS = 'table-hider';
const TABLE_HIDER_VISIBLE_CLASS = 'table-hider--visible';

const TABLE_PILL_CLASS = 'table-pill';

const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell';
const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan';

const HIDE_COLUMNS_INITIALIZED = 'uw-hide-columns--initialized';

@Utility({
  selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`,
})
export class HideColumns {

  _storageManager = new StorageManager('HIDE_COLUMNS', '1.1.0', { location: LOCATION.LOCAL });
  _eventManager;

  _element;
  _elementWrapper;
  _tableUtilContainer;

  _autoHide;

  _tableIndices;

  headerToHider = new Map();
  hiderToHeader = new Map();

  addHeaderHider(th, hider) {
    this.headerToHider.set(th, hider);
    this.hiderToHeader.set(hider, th);
  }

  constructor(element) {
    this._autoHide  = this._storageManager.load('autoHide', {}) || false;

    if (!element)
      throw new Error('Hide Columns utility cannot be setup without an element!');

    // do not provide hide-column ability in tables inside modals, async forms with response or tail.datetime instances
    if (element.closest('[uw-modal], .async-form__response, .tail-datetime-calendar'))
      return false;

    if (element.classList.contains(HIDE_COLUMNS_INITIALIZED))
      return false;

    this._element = element;
    this._eventManager = new EventManager();

    this._tableIndices = new TableIndices(this._element);

    const hideColumnsContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}]`);
    if (!hideColumnsContainer) {
      throw new Error('Hide Columns utility needs to be setup on a table inside a hide columns container!');
    }
    this._elementWrapper = hideColumnsContainer;

    // get or create table utils container
    this._tableUtilContainer = hideColumnsContainer.querySelector(TABLE_UTILS_CONTAINER_SELECTOR);
    if (!this._tableUtilContainer) {
      this._tableUtilContainer = document.createElement('div');
      this._tableUtilContainer.setAttribute(TABLE_UTILS_ATTR, '');
      const tableContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}] > *`);
      hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer);
    }

    [...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.setupHideButton(th));

    this._eventManager.registerNewMutationObserver(this._tableMutated.bind(this), this._element, { childList: true, subtree: true });

    this._element.classList.add(HIDE_COLUMNS_INITIALIZED);
  }

  destroy() {
    this._eventManager.cleanUp();
    this._tableUtilContainer.remove();
    this._element.classList.remove(HIDE_COLUMNS_INITIALIZED);
  }

  setupHideButton(th) {
    const preHidden = this.isHiddenTH(th);

    const hider = document.createElement('span');

    const hiderIcon = document.createElement('i');
    hiderIcon.classList.add('fas', 'fa-fw');
    hider.appendChild(hiderIcon);

    const hiderContent = document.createElement('span');
    hiderContent.classList.add('table-hider__label');
    hiderContent.innerHTML = th.getAttribute(HIDE_COLUMNS_HIDER_LABEL) || th.innerText;
    hider.appendChild(hiderContent);

    this.addHeaderHider(th, hider);

    const mouseOverEvent = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => {
      hider.classList.add(TABLE_HIDER_VISIBLE_CLASS);
    }).bind(this), th);
    this._eventManager.registerNewListener(mouseOverEvent);

    const mouseOutEvent = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => {
      if (hider.classList.contains(TABLE_HIDER_CLASS)) {
        hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
      }
    }).bind(this), th);
    this._eventManager.registerNewListener(mouseOutEvent);

    const hideClickEvent = new EventWrapper(EVENT_TYPE.CLICK, ((event) => {
      event.preventDefault();
      event.stopPropagation();
      this.switchColumnDisplay(th);
      // this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider));
    }).bind(this), hider);
    this._eventManager.registerNewListener(hideClickEvent);

    const mouseOverHider = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => {
      hider.classList.add(TABLE_HIDER_VISIBLE_CLASS);
      const currentlyHidden = this.hiderStatus(th);
      this.updateHiderIcon(hider, !currentlyHidden);
    }).bind(this), hider);
    this._eventManager.registerNewListener(mouseOverHider);

    const mouseOutHider = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => {
      if (hider.classList.contains(TABLE_HIDER_CLASS)) {
        hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
      }
      const currentlyHidden = this.hiderStatus(th);
      this.updateHiderIcon(hider, currentlyHidden);
    }).bind(this), hider);
    this._eventManager.registerNewListener(mouseOutHider);

    new ResizeObserver(() => { this.repositionHider(hider); }).observe(th);

    // reposition hider on each window resize event
    // window.addEventListener('resize', () => this.repositionHider(hider));

    this.switchColumnDisplay(th, preHidden);
  }

  switchColumnDisplay(th, hidden) {
    hidden = typeof(hidden) === 'undefined' ? !this.isHiddenTH(th) : !!hidden;

    Array.from(this.cellColumns(th)).forEach(columnIndex => this.updateColumnDisplay(columnIndex, hidden));
  }

  updateColumnDisplay(columnIndex, hidden) {
    // console.debug('updateColumnDisplay', { columnIndex, hidden });
    Array.from(this._element.rows).forEach(row => {
      const cell = this.getCol(row, columnIndex);

      if (cell) {
        const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN);
        const colspan = Math.max(1, cell.colSpan) || 1;
        const visibleColumns = this.cellColumns(cell).reduce((count, cColumnIndex) => (cColumnIndex === columnIndex ? hidden : this.isHiddenColumn(cColumnIndex)) ? count : count + 1, 0);

        // if (cell.tagName === 'TH') {
        //   console.debug({cell, originalColspan, colspan, visibleColumns, isHidden: cell.classList.contains(CELL_HIDDEN_CLASS)});
        // }


        if (visibleColumns <= 0) {
          cell.classList.add(CELL_HIDDEN_CLASS);
        } else {
          cell.classList.remove(CELL_HIDDEN_CLASS);

          if (colspan !== visibleColumns) {
            if (!originalColspan) {
              cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan);
            }
            cell.colSpan = visibleColumns;
          }
        }
      }
    });

    const touchedColumns = new Array();

    this.columnTHs(columnIndex)
      .forEach(th => {
        touchedColumns.push(...this.cellColumns(th));

        if (!this._element.classList.contains(HIDE_COLUMNS_INITIALIZED))
          return;

        const thHidden = this.cellColumns(th).every(cColumnIndex => cColumnIndex === columnIndex ? hidden : this.isHiddenColumn(cColumnIndex));
        // persist new hidden setting for column
        if (thHidden == this.isDefaultHiddenTH(th)) {
          this._storageManager.remove(this.getStorageKey(th));
        } else {
          this._storageManager.save(this.getStorageKey(th), thHidden);
        }
      });

    touchedColumns.flatMap(cColumnIndex => this.columnTHs(cColumnIndex))
      .forEach(th => {
        const hider = this.headerToHider.get(th);
        if (!hider)
          return;
        
        this.updateHider(hider, this.hiderStatus(th));
      });
  }

  updateHider(hider, hidden) {
    // console.debug({hider, hidden, columnIndex: this.colIndex(this.hiderToHeader.get(hider)), colSpan: this.colSpan(this.hiderToHeader.get(hider))});
    
    if (hidden) {
      hider.classList.remove(TABLE_HIDER_CLASS);
      hider.classList.add(TABLE_PILL_CLASS);
      this._tableUtilContainer.appendChild(hider);
    } else {
      hider.classList.remove(TABLE_PILL_CLASS);
      hider.classList.add(TABLE_HIDER_CLASS);
      this.hideHiderBehindHeader(hider);
    }
    this.updateHiderIcon(hider, hidden);
  }

  updateHiderIcon(hider, hidden) {
    Array.from(hider.getElementsByClassName('fas')).forEach(hiderIcon => {
      hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash');
      hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye');
    });
  }

  hideHiderBehindHeader(hider) {
    if (!this.hiderToHeader.get(hider).contains(hider)) {
      this.hiderToHeader.get(hider).appendChild(hider);
    }

    this.repositionHider(hider);

    // remove visible class if necessary
    hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
  }

  repositionHider(hider) {
    const thR = this.hiderToHeader.get(hider).getBoundingClientRect(),
          hR  = hider.getBoundingClientRect();

    hider.style.left = (thR.width/2 - hR.width/2) + 'px';
    hider.style.top  = thR.height + 'px';
  }

  _tableMutated(mutationList) {
    if (!Array.from(mutationList).some(mutation => mutation.type === 'childList'))
      return;

    if (Array.from(mutationList).every(mRecord => mRecord.type === 'childList' && [...mRecord.addedNodes, ...mRecord.removedNodes].every(isTableHider)))
      return;

    // console.debug('_tableMutated', { mutationList });

    this._tableIndices = new TableIndices(this._element, { colSpan: this.colSpan.bind(this) });

    Array.from(this._element.rows)
      .flatMap(row => Array.from(row.cells))
      .filter(th => th.tagName === 'TH' && !th.hasAttribute(HIDE_COLUMNS_NO_HIDE))
      .forEach(th => this.updateColumnDisplay(this.colIndex(th), this.isHiddenTH(th)));
  }

  getStorageKey(th) {
    // get handler name
    const handlerIdent = document.querySelector('[uw-handler]').getAttribute('uw-handler');

    // get hide-columns container ident (if not present, use table index in document as fallback)
    let tIdent = this._elementWrapper.getAttribute(HIDE_COLUMNS_CONTAINER_IDENT);
    if (!tIdent) {
      const tablesInDocument = document.getElementsByTagName('TABLE');
      for (let i = 0; i < tablesInDocument.length; i++) {
        if (tablesInDocument[i] === this._element) {
          tIdent = i;
          break;
        }
      }
    }

    // check for unique table header ident from backend (if not present, use cell index as fallback)
    let thIdent = th.getAttribute(TABLE_HEADER_IDENT);
    if (!thIdent) {
      thIdent = this.colIndex(th);
    }

    return `${handlerIdent}__${tIdent}__${thIdent}`;
  }

  isEmptyColumn(columnIndex) {
    for (let row of this._element.rows) {
      const cell = this.getCol(row, columnIndex);
      if (!cell || cell.tagName == 'TH')
        continue;
      if (cell.querySelector('.table__td-content')) {
        for (let child of cell.children) {
          if (!isEmptyElement(child))
            return false;
        }
        return true;
      } else {
        return isEmptyElement(cell);
      }
    }
  }

  columnTHs(columnIndex) {
    return Array.from(this._element.rows)
      .map(row => this.getCol(row, columnIndex))
      .filter(cell => cell && cell.tagName === 'TH');
  }

  cellColumns(cell) {
    const columnIndex = this.colIndex(cell);
    return Array.from(new Array(this.colSpan(cell)), (_x, i) => columnIndex + i);
  }

  isHiddenTH(th) {
    return this.cellColumns(th).every(columnIndex => this.isHiddenColumn(columnIndex));
  }

  hiderStatus(th) {
    const columnsHidden = this.isHiddenTH(th);
    const shadowed = this.cellColumns(th).every(columnIndex => this.isHiddenColumn(columnIndex, this.columnTHs(columnIndex).filter(oTH => oTH !== th)));
    const isFirst = this.cellColumns(th).some(columnIndex => this.columnTHs(columnIndex)[0] === th);

    // console.debug("hiderStatus", { th, columnsHidden, shadowed, isFirst });

    return columnsHidden && (!shadowed || isFirst);
  }
  
  isDefaultHiddenTH(th) {
    return this.cellColumns(th).every(columnIndex => this.isDefaultHiddenColumn(columnIndex, Array.of(th)));
  }

  isHiddenColumn(columnIndex, ths) {
    ths = ths === undefined ? this.columnTHs(columnIndex) : ths;

    const hidden = ths.map(th => this._storageManager.load(this.getStorageKey(th)));

    return hidden.every(h => h === undefined) ? this.isDefaultHiddenColumn(columnIndex, ths) : hidden.some(h => h);
  }

  isDefaultHiddenColumn(columnIndex, ths) {
    ths = ths === undefined ? this.columnTHs(columnIndex) : ths;

    return this.isEmptyColumn(columnIndex) && this._autoHide || ths.some(th => th.hasAttribute(HIDE_COLUMNS_DEFAULT_HIDDEN));
  }

  colSpan(cell) {
    if (!cell)
      return 1;
    
    const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN);
    const colspan = Math.max(1, cell.colSpan) || 1;

    return originalColspan ? Math.max(colspan, originalColspan) : colspan;
  }

  colIndex(cell) {
    return this._tableIndices.colIndex(cell);
  }

  getCol(row, col) {
    return this._tableIndices.getCell(row.rowIndex, col);
  }
}

function isEmptyElement(element) {
  for (let child of element.childNodes) {
    if (child.nodeName !== '#comment')
      return false;
  }
  return true;
}

function isTableHider(element) {
  return element && element.classList && (
       element.classList.contains(TABLE_HIDER_CLASS)
    || element.classList.contains(TABLE_HIDER_VISIBLE_CLASS)
    || element.classList.contains(TABLE_PILL_CLASS)
  );
}
