import { Utility } from '../../core/utility';
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
import { Datepicker } from '../form/datepicker';
import { HttpClient } from '../../services/http-client/http-client';
import * as debounce from 'lodash.debounce';
import * as throttle from 'lodash.throttle';
import './async-table-filter.sass';
import './async-table.sass';

const ATTR_SUBMIT_LOCKED = 'submit-locked';

const INPUT_DEBOUNCE = 600;
const FILTER_DEBOUNCE = 100;
const HEADER_HEIGHT = 80;

const ASYNC_TABLE_STORAGE_KEY = 'ASYNC_TABLE';
const ASYNC_TABLE_STORAGE_VERSION = '2.0.0';

const ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable';
const ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized';
const ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';

const ASYNC_TABLE_FILTER_FORM_SELECTOR = '.table-filter-form';

@Utility({
  selector: '[uw-async-table]',
})
export class AsyncTable {

  _element;
  _app;

  _asyncTableHeader;
  _asyncTableId;

  _ths = [];
  _pageLinks = [];
  _pagesizeForm;
  _scrollTable;
  _cssIdPrefix = '';

  _cancelPendingUpdates = [];

  _tableFilterInputs = {
    search: [],
    input: [],
    change: [],
    select: [],
  };
  _ignoreRequest = false;

  _windowStorage;
  _historyStorage;

  _active = true;

  constructor(element, app) {
    if (!element) {
      throw new Error('Async Table utility cannot be setup without an element!');
    }

    if (!app) {
      throw new Error('Async Table utility cannot be setup without an app!');
    }

    this._element = element;
    this._app = app;

    if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) {
      return false;
    }
      
    // param asyncTableDbHeader
    if (this._element.dataset.asyncTableDbHeader !== undefined) {
      this._asyncTableHeader = this._element.dataset.asyncTableDbHeader;
    }

    const table = this._element.querySelector('table, .div__course-teaser');
    if (!table) {
      throw new Error('Async Table utility needs a <table> or a <div .div__course-teaser> in its element!');
    }

    const rawTableId = table.id;
    this._cssIdPrefix = findCssIdPrefix(rawTableId);
    this._asyncTableId = rawTableId.replace(this._cssIdPrefix, '');

    if (!this._asyncTableId) {
      throw new Error('Async Table cannot be set up without an ident!');
    }

    this._windowStorage = new StorageManager([ASYNC_TABLE_STORAGE_KEY, this._asyncTableId], ASYNC_TABLE_STORAGE_VERSION, { location: LOCATION.WINDOW });
    this._historyStorage = new StorageManager([ASYNC_TABLE_STORAGE_KEY, this._asyncTableId], ASYNC_TABLE_STORAGE_VERSION, { location: LOCATION.HISTORY });

    // find scrolltable wrapper
    this._scrollTable = this._element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
    if (!this._scrollTable) {
      throw new Error('Async Table cannot be set up without a scrolltable element!');
    }

    this._processStorage();

    this._setupTableFilter();

    this._windowStorage.remove('currentTableUrl');

    if (!('currentTableUrl' in this._element.dataset)) {
      this._element.dataset['currentTableUrl'] = document.location.href;
      this._historyStorage.save('currentTableUrl', document.location.href, { location: LOCATION.HISTORY, history: { push: false } });
    }

    this._historyListener();

    this._historyStorage.addHistoryListener(this._historyListener.bind(this));

    // mark initialized
    if (this._active)
      this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
  }

  _historyListener(historyState) {
    if (!this._active)
      return;
    
    const windowUrl = this._element.dataset['currentTableUrl'];
    const historyUrl = historyState ? historyState['currentTableUrl'] : this._historyStorage.load('currentTableUrl');
    this._debugLog('_historyListener', historyState, windowUrl, historyUrl);

    if (this._isEquivalentUrl(windowUrl, historyUrl))
      return;
    
    this._debugLog('_historyListener', historyUrl);
    this._updateTableFrom(historyUrl || document.location.href, undefined, true);
  }

  _isEquivalentUrl(a, b) {
    return a === b;
  }

  start() {
    this._startSortableHeaders();
    this._startPagination();
    this._startPageSizeSelect();
    this._startTableFilter();
  }

  destroy() {
    console.log('TBD: Destroy AsyncTable');
  }

  _startSortableHeaders() {
    this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable, .course-header'))
      .map((th) => ({ element: th }));

    this._ths.forEach((th) => {
      th.clickHandler = (event) => {
        this._windowStorage.save('horizPos', (this._scrollTable || {}).scrollLeft);
        this._linkClickHandler(event);
      };
      th.element.addEventListener('click', th.clickHandler);
    });
  }

  _startPagination() {
    const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination');
    if (pagination) {
      this._pageLinks = Array.from(pagination.querySelectorAll('.page-link'))
        .map((link) => ({ element: link }));

      this._pageLinks.forEach((link) => {
        link.clickHandler = (event) => {
          const tableBoundingRect = this._scrollTable.getBoundingClientRect();
          if (tableBoundingRect.top < HEADER_HEIGHT) {
            const scrollTo = {
              top: (this._scrollTable.offsetTop || 0) - HEADER_HEIGHT,
              left: this._scrollTable.offsetLeft || 0,
              behavior: 'smooth',
            };
            this._windowStorage.save('scrollTo', scrollTo);
          }
          this._linkClickHandler(event);
        };
        link.element.addEventListener('click', link.clickHandler);
      });
    }
  }

  _startPageSizeSelect() {
    // pagesize form
    this._pagesizeForm = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagesize-form');

    if (this._pagesizeForm) {
      const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
      pagesizeSelect.addEventListener('change', this._changePagesizeHandler);
    }
  }

  _setupTableFilter() {
    const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
    if (tableFilterForm) {
      this._gatherTableFilterInputs(tableFilterForm);
    }
  }

  _startTableFilter() {
    const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
    if (tableFilterForm) {
      this._addTableFilterEventListeners(tableFilterForm);
    }
  }

  _gatherTableFilterInputs(tableFilterForm) {
    Array.from(tableFilterForm.querySelectorAll('input')).forEach((input) => {
      const inputType = input.getAttribute('type');
      if (inputType === 'search') {
        this._tableFilterInputs.search.push(input);
      } else if (['text','date','time','datetime-local'].includes(inputType)) {
        this._tableFilterInputs.input.push(input);
      } else {
        this._tableFilterInputs.change.push(input);
      }
    });

    Array.from(tableFilterForm.querySelectorAll('select')).forEach((input) => this._tableFilterInputs.select.push(input));
  }

  _addTableFilterEventListeners(tableFilterForm) {
    const debouncedUpdateFromTableFilter = throttle((() => this._updateFromTableFilter(tableFilterForm)).bind(this), FILTER_DEBOUNCE, { leading: true, trailing: false });

    [...this._tableFilterInputs.search, ...this._tableFilterInputs.input].forEach((input) => {
      const submitLockObserver = new MutationObserver((mutations, observer) => {
        for (const mutation of mutations) {
          // if the submit lock has been released, trigger an update and disconnect this observer
          if (mutation.target === input && mutation.attributeName === ATTR_SUBMIT_LOCKED && mutation.oldValue === 'true' && mutation.target.getAttribute(mutation.attributeName) === 'false') {
            debouncedUpdateFromTableFilter();
            observer.disconnect();
            break;
          }
        }
      });
      this._cancelPendingUpdates.push(() => { submitLockObserver.disconnect(); });

      const debouncedInput = debounce(() => {
        const submitLockedAttr = input.getAttribute(ATTR_SUBMIT_LOCKED);
        const submitLocked = submitLockedAttr === 'true';
        if (!submitLocked && (input.value.length === 0 || input.value.length > 2)) {
          debouncedUpdateFromTableFilter();
        } else if (submitLockedAttr === 'true') {
          // observe the submit lock of the input element
          submitLockObserver.observe(input, {
            attributes: true,
            attributeFilter: [ATTR_SUBMIT_LOCKED],
            attributeOldValue: true,
          });
        }
      }, INPUT_DEBOUNCE);
      this._cancelPendingUpdates.push(debouncedInput.cancel);

      input.addEventListener('input', () => {
        this._ignoreRequest = true;
        debouncedInput();
      });
    });

    this._tableFilterInputs.change.forEach((input) => {
      input.addEventListener('change', () => {
        //if (this._element.classList.contains(ASYNC_TABLE_LOADING_CLASS))
        this._ignoreRequest = true;
        debouncedUpdateFromTableFilter();
      });
    });

    this._tableFilterInputs.select.forEach((input) => {
      input.addEventListener('change', () => {
        this._ignoreRequest = true;
        debouncedUpdateFromTableFilter();
      });
    });

    tableFilterForm.addEventListener('submit', (event) =>{
      event.preventDefault();
      this._ignoreRequest = true;
      debouncedUpdateFromTableFilter();
    });
  }

  _updateFromTableFilter(tableFilterForm) {
    const url = this._serializeTableFilterToURL(tableFilterForm);
    let callback = null;

    const focusedInput = tableFilterForm.querySelector(':focus, :active');
    // focus previously focused input
    if (focusedInput && focusedInput.selectionStart !== null) {
      const selectionStart = focusedInput.selectionStart;
      // remove the following part of the id to get rid of the random
      // (yet somewhat structured) prefix we got from nudging.
      const prefix = findCssIdPrefix(focusedInput.id);
      const focusId = focusedInput.id.replace(prefix, '');
      callback = function(wrapper) {
        const idPrefix = this._windowStorage.load('cssIdPrefix');
        const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
        if (toBeFocused) {
          toBeFocused.focus();
          toBeFocused.selectionStart = selectionStart;
        }
      };
    }
    this._updateTableFrom(url, callback && callback.bind(this));
  }

  _serializeTableFilterToURL(tableFilterForm) {
    const url = new URL(this._windowStorage.load('currentTableUrl') || window.location.href);

    // create new FormData and format any date values
    const formData = Datepicker.unformatAll(tableFilterForm, new FormData(tableFilterForm));

    this._debugLog('_serializeTableFilterToURL', Array.from(formData.entries()), url.href);

    const searchParams = new URLSearchParams(Array.from(formData.entries()));
    url.search = searchParams.toString();

    this._debugLog('_serializeTableFilterToURL', url.href);

    return url;
  }

  _processStorage() {
    const scrollTo = this._windowStorage.load('scrollTo');
    if (scrollTo && this._scrollTable) {
      window.scrollTo(scrollTo);
    }
    this._windowStorage.remove('scrollTo');

    const horizPos = this._windowStorage.load('horizPos');
    if (horizPos && this._scrollTable) {
      this._scrollTable.scrollLeft = horizPos;
    }
    this._windowStorage.remove('horizPos');
  }

  _removeListeners() {
    this._ths.forEach(function(th) {
      th.element.removeEventListener('click', th.clickHandler);
    });

    this._pageLinks.forEach(function(link) {
      link.element.removeEventListener('click', link.clickHandler);
    });

    if (this._pagesizeForm) {
      const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
      pagesizeSelect.removeEventListener('change', this._changePagesizeHandler);
    }
  }

  _linkClickHandler = (event) => {
    event.preventDefault();
    let url = this._getClickDestination(event.target);
    if (!url)
      return;

    if (!url.match(/^http/)) {
      url = window.location.origin + window.location.pathname + url;
    }
    this._updateTableFrom(url);
  }

  _getClickDestination(el) {
    if (!el.matches('a') && !el.querySelector('a')) {
      return null;
    }
    return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
  }

  _changePagesizeHandler = () => {
    const url = new URL(this._windowStorage.load('currentTableUrl') || window.location.href);

    // create new FormData and format any date values
    const formData = Datepicker.unformatAll(this._pagesizeForm, new FormData(this._pagesizeForm));

    for (const k of url.searchParams.keys()) {
      url.searchParams.delete(k);
    }

    for (const kv of formData.entries()) {
      url.searchParams.append(kv[0], kv[1]);
    }

    this._updateTableFrom(url.href);
  }

  // fetches new sorted element from url with params and replaces contents of current element
  _updateTableFrom(url, callback, isPopState) {
    url = new URL(url);

    const cancelPendingUpdates = (() => {
      this._cancelPendingUpdates.forEach(f => f());
    }).bind(this);
    [0, INPUT_DEBOUNCE * 1.5, FILTER_DEBOUNCE * 1.5].forEach(delay => window.setTimeout(cancelPendingUpdates, delay));
    this._ignoreRequest = false;
    
    this._element.classList.add(ASYNC_TABLE_LOADING_CLASS);

    const headers = {
      'Accept': HttpClient.ACCEPT.TEXT_HTML,
      [this._asyncTableHeader]: this._asyncTableId,
    };

    this._app.httpClient.get({
      url: url,
      headers: headers,
    }).then(
      (response) => this._app.htmlHelpers.parseResponse(response),
    ).then((response) => {
        // check if request should be ignored
      if (this._ignoreRequest) {
        return false;
      }

      if (!isPopState)
        this._historyStorage.save('currentTableUrl', url.href, { location: LOCATION.HISTORY, history: { push: true, url: response.headers.get('DB-Table-Canonical-URL') || url.href } });
      
      this._windowStorage.save('currentTableUrl', url.href);
      // reset table
      this._removeListeners();
      this._active = false;
      this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
      this._element.dataset['currentTableUrl'] = url.href;
      // update table with new
      this._element.innerHTML = response.element.innerHTML;

      this._app.utilRegistry.initAll(this._element);

      if (callback && typeof callback === 'function') {
        this._windowStorage.save('cssIdPrefix', response.idPrefix);
        callback(this._element);
        this._windowStorage.remove('cssIdPrefix');
      }
    }).catch((err) => console.error(err)
    ).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS));
  }

  _debugLog() {}
  // _debugLog(fName, ...args) {
  //   console.log(`[DEBUGLOG] AsyncTable.${fName}`, { args: args, instance: this });
  // }
}


// returns any random nudged prefix found in the given id
function findCssIdPrefix(id) {
  const matcher = /r\d*?__/;
  const maybePrefix = id.match(matcher);
  if (maybePrefix && maybePrefix[0]) {
    return maybePrefix[0];
  }
  return '';
}
