import Handsontable from './../browser';
import {addClass, outerHeight, outerWidth} from './../helpers/dom/element';
import {arrayEach} from './../helpers/array';
import {objectEach} from './../helpers/object';
import {rangeEach} from './../helpers/number';
import {stringify} from './../helpers/mixed';

/**
 * @class GhostTable
 * @util
 */
class GhostTable {
  constructor(hotInstance) {
    /**
     * Handsontable instance.
     *
     * @type {Core}
     */
    this.hot = hotInstance;
    /**
     * Container element where every table will be injected.
     *
     * @type {HTMLElement|null}
     */
    this.container = null;
    /**
     * Flag which determine is table was injected to DOM.
     *
     * @type {Boolean}
     */
    this.injected = false;
    /**
     * Added rows collection.
     *
     * @type {Array}
     */
    this.rows = [];
    /**
     * Added columns collection.
     *
     * @type {Array}
     */
    this.columns = [];
    /**
     * Samples prepared for calculations.
     *
     * @type {Map}
     * @default {null}
     */
    this.samples = null;
    /**
     * Ghost table settings.
     *
     * @type {Object}
     * @default {Object}
     */
    this.settings = {
      useHeaders: true
    };
  }

  /**
   * Add row.
   *
   * @param {Number} row Row index.
   * @param {Map} samples Samples Map object.
   */
  addRow(row, samples) {
    if (this.columns.length) {
      throw new Error('Doesn\'t support multi-dimensional table');
    }
    if (!this.rows.length) {
      this.container = this.createContainer(this.hot.rootElement.className);
    }
    const rowObject = {row: row};
    this.rows.push(rowObject);

    this.samples = samples;
    this.table = this.createTable(this.hot.table.className);
    this.table.colGroup.appendChild(this.createColGroupsCol());
    this.table.tr.appendChild(this.createRow(row));
    this.container.container.appendChild(this.table.fragment);

    rowObject.table = this.table.table;
  }

  /**
   * Add a row consisting of the column headers.
   */
  addColumnHeadersRow(samples) {
    if (this.hot.getColHeader(0) != null) {
      const rowObject = {row: -1};
      this.rows.push(rowObject);

      this.container = this.createContainer(this.hot.rootElement.className);

      this.samples = samples;
      this.table = this.createTable(this.hot.table.className);
      this.table.colGroup.appendChild(this.createColGroupsCol());
      this.table.tHead.appendChild(this.createColumnHeadersRow());
      this.container.container.appendChild(this.table.fragment);

      rowObject.table = this.table.table;
    }
  }

  /**
   * Add column.
   *
   * @param {Number} column Column index.
   * @param {Map} samples Samples Map object.
   */
  addColumn(column, samples) {
    if (this.rows.length) {
      throw new Error('Doesn\'t support multi-dimensional table');
    }
    if (!this.columns.length) {
      this.container = this.createContainer(this.hot.rootElement.className);
    }
    const columnObject = {col: column};
    this.columns.push(columnObject);

    this.samples = samples;
    this.table = this.createTable(this.hot.table.className);

    if (this.getSetting('useHeaders') && this.hot.getColHeader(column) !== null) {
      this.hot.view.appendColHeader(column, this.table.th);
    }
    this.table.tBody.appendChild(this.createCol(column));
    this.container.container.appendChild(this.table.fragment);

    columnObject.table = this.table.table;
  }

  /**
   * Get calculated heights.
   *
   * @param {Function} callback Callback which will be fired for each calculated row.
   */
  getHeights(callback) {
    if (!this.injected) {
      this.injectTable();
    }
    arrayEach(this.rows, (row) => {
      // -1 <- reduce border-top from table
      callback(row.row, outerHeight(row.table) - 1);
    });
  }

  /**
   * Get calculated widths.
   *
   * @param {Function} callback Callback which will be fired for each calculated column.
   */
  getWidths(callback) {
    if (!this.injected) {
      this.injectTable();
    }
    arrayEach(this.columns, (column) => {
      callback(column.col, outerWidth(column.table));
    });
  }

  /**
   * Set the Ghost Table settings to the provided object.
   *
   * @param {Object} settings New Ghost Table Settings
   */
  setSettings(settings) {
    this.settings = settings;
  }

  /**
   * Set a single setting of the Ghost Table.
   *
   * @param {String} name Setting name.
   * @param {*} value Setting value.
   */
  setSetting(name, value) {
    if (!this.settings) {
      this.settings = {};
    }

    this.settings[name] = value;
  }

  /**
   * Get the Ghost Table settings.
   *
   * @returns {Object|null}
   */
  getSettings() {
    return this.settings;
  }

  /**
   * Get a single Ghost Table setting.
   *
   * @param {String} name
   * @returns {Boolean|null}
   */
  getSetting(name) {
    if (this.settings) {
      return this.settings[name];
    } else {
      return null;
    }
  }

  /**
   * Create colgroup col elements.
   *
   * @returns {DocumentFragment}
   */
  createColGroupsCol() {
    const d = document;
    const fragment = d.createDocumentFragment();

    if (this.hot.hasRowHeaders()) {
      fragment.appendChild(this.createColElement(-1));
    }

    this.samples.forEach((sample) => {
      arrayEach(sample.strings, (string) => {
        fragment.appendChild(this.createColElement(string.col));
      });
    });

    return fragment;
  }

  /**
   * Create table row element.
   *
   * @param {Number} row Row index.
   * @returns {DocumentFragment} Returns created table row elements.
   */
  createRow(row) {
    const d = document;
    const fragment = d.createDocumentFragment();
    const th = d.createElement('th');

    if (this.hot.hasRowHeaders()) {
      this.hot.view.appendRowHeader(row, th);

      fragment.appendChild(th);
    }

    this.samples.forEach((sample) => {
      arrayEach(sample.strings, (string) => {
        let column = string.col;
        let cellProperties = this.hot.getCellMeta(row, column);

        cellProperties.col = column;
        cellProperties.row = row;

        let renderer = this.hot.getCellRenderer(cellProperties);
        const td = d.createElement('td');

        renderer(this.hot, td, row, column, this.hot.colToProp(column), string.value, cellProperties);
        fragment.appendChild(td);
      });
    });

    return fragment;
  }

  createColumnHeadersRow() {
    const d = document;
    const fragment = d.createDocumentFragment();

    if (this.hot.hasRowHeaders()) {
      const th = d.createElement('th');
      this.hot.view.appendColHeader(-1, th);
      fragment.appendChild(th);
    }

    this.samples.forEach((sample) => {
      arrayEach(sample.strings, (string) => {
        let column = string.col;

        const th = d.createElement('th');

        this.hot.view.appendColHeader(column, th);
        fragment.appendChild(th);
      });
    });

    return fragment;
  }

  /**
   * Create table column elements.
   *
   * @param {Number} column Column index.
   * @returns {DocumentFragment} Returns created column table column elements.
   */
  createCol(column) {
    const d = document;
    const fragment = d.createDocumentFragment();

    this.samples.forEach((sample) => {
      arrayEach(sample.strings, (string) => {
        let row = string.row;
        let cellProperties = this.hot.getCellMeta(row, column);

        cellProperties.col = column;
        cellProperties.row = row;

        let renderer = this.hot.getCellRenderer(cellProperties);
        const td = d.createElement('td');
        const tr = d.createElement('tr');

        renderer(this.hot, td, row, column, this.hot.colToProp(column), string.value, cellProperties);
        tr.appendChild(td);
        fragment.appendChild(tr);
      });
    });

    return fragment;
  }

  /**
   * Remove table from document and reset internal state.
   */
  clean() {
    this.rows.length = 0;
    this.rows[-1] = void 0;
    this.columns.length = 0;

    if (this.samples) {
      this.samples.clear();
    }
    this.samples = null;
    this.removeTable();
  }

  /**
   * Inject generated table into document.
   *
   * @param {HTMLElement} [parent=null]
   */
  injectTable(parent = null) {
    if (!this.injected) {
      (parent || this.hot.rootElement).appendChild(this.container.fragment);
      this.injected = true;
    }
  }

  /**
   * Remove table from document.
   */
  removeTable() {
    if (this.injected && this.container.container.parentNode) {
      this.container.container.parentNode.removeChild(this.container.container);
      this.container = null;
      this.injected = false;
    }
  }

  /**
   * Create col element.
   *
   * @param {Number} column Column index.
   * @returns {HTMLElement}
   */
  createColElement(column) {
    const d = document;
    const col = d.createElement('col');

    col.style.width = this.hot.view.wt.wtTable.getStretchedColumnWidth(column) + 'px';

    return col;
  }

  /**
   * Create table element.
   *
   * @param {String} className
   * @returns {Object}
   */
  createTable(className = '') {
    const d = document;
    const fragment = d.createDocumentFragment();
    const table = d.createElement('table');
    const tHead = d.createElement('thead');
    const tBody = d.createElement('tbody');
    const colGroup = d.createElement('colgroup');
    const tr = d.createElement('tr');
    const th = d.createElement('th');

    if (this.isVertical()) {
      table.appendChild(colGroup);
    }
    if (this.isHorizontal()) {
      tr.appendChild(th);
      tHead.appendChild(tr);
      table.style.tableLayout = 'auto';
      table.style.width = 'auto';
    }
    table.appendChild(tHead);

    if (this.isVertical()) {
      tBody.appendChild(tr);
    }
    table.appendChild(tBody);
    addClass(table, className);
    fragment.appendChild(table);

    return {fragment, table, tHead, tBody, colGroup, tr, th};
  }

  /**
   * Create container for tables.
   *
   * @param {String} className
   * @returns {Object}
   */
  createContainer(className = '') {
    const d = document;
    const fragment = d.createDocumentFragment();
    const container = d.createElement('div');

    className = 'htGhostTable htAutoSize ' + className.trim();
    addClass(container, className);
    fragment.appendChild(container);

    return {fragment, container};
  }

  /**
   * Checks if table is raised vertically (checking rows).
   *
   * @returns {Boolean}
   */
  isVertical() {
    return this.rows.length && !this.columns.length ? true : false;
  }

  /**
   * Checks if table is raised horizontally (checking columns).
   *
   * @returns {Boolean}
   */
  isHorizontal() {
    return this.columns.length && !this.rows.length ? true : false;
  }
}

export {GhostTable};

// temp for tests only!
Handsontable.utils.GhostTable = GhostTable;
