import Vue from 'vue';
import debounce from 'throttle-debounce/debounce';
import merge from 'element-ui/src/utils/merge';
import { orderBy, getColumnById, getRowIdentity, getColumnByKey } from './util';

const sortData = (data, states) => {
  const sortingColumn = states.sortingColumn;
  if (states.isServerTable || !sortingColumn || typeof sortingColumn.sortable === 'string') {
    return data;
  }
  return orderBy(data, states.sortProp, states.sortOrder, sortingColumn.sortMethod, sortingColumn.sortBy);
};
const showData = (data, states) => {
  if (!states.isPageTable || states.isServerTable) {
    return data;
  }
  return data.slice((states.currentPage - 1) * states.pageSize, states.currentPage * states.pageSize);
};
const getKeysMap = function(array, rowKey) {
  const arrayMap = {};
  (array || []).forEach((row, index) => {
    arrayMap[getRowIdentity(row, rowKey)] = { row, index };
  });
  return arrayMap;
};

const toggleRowSelection = function(states, row, selected) {
  let changed = false;
  const selection = states.selection;
  const index = selection.indexOf(row);
  if (typeof selected === 'undefined') {
    if (index === -1) {
      selection.push(row);
      changed = true;
    } else {
      selection.splice(index, 1);
      changed = true;
    }
  } else {
    if (selected && index === -1) {
      selection.push(row);
      changed = true;
    } else if (!selected && index > -1) {
      selection.splice(index, 1);
      changed = true;
    }
  }

  return changed;
};

const toggleRowExpansion = function(states, row, expanded) {
  let changed = false;
  const expandRows = states.expandRows;
  if (typeof expanded !== 'undefined') {
    const index = expandRows.indexOf(row);
    if (expanded) {
      if (index === -1) {
        expandRows.push(row);
        changed = true;
      }
    } else {
      if (index !== -1) {
        expandRows.splice(index, 1);
        changed = true;
      }
    }
  } else {
    const index = expandRows.indexOf(row);
    if (index === -1) {
      expandRows.push(row);
      changed = true;
    } else {
      expandRows.splice(index, 1);
      changed = true;
    }
  }

  return changed;
};

const TableStore = function(table, initialState = {}) {
  if (!table) {
    throw new Error('Table is required.');
  }
  this.table = table;

  this.states = {
    rowKey: null,
    _columns: [],
    originColumns: [],
    columns: [],
    fixedColumns: [],
    rightFixedColumns: [],
    leafColumns: [],
    fixedLeafColumns: [],
    rightFixedLeafColumns: [],
    leafColumnsLength: 0,
    fixedLeafColumnsLength: 0,
    rightFixedLeafColumnsLength: 0,
    isComplex: false,
    filteredData: null,
    data: null,
    sortingColumn: null,
    sortProp: null,
    sortOrder: null,
    isAllSelected: false,
    selection: [],
    reserveSelection: false,
    selectable: null,
    currentRow: null,
    hoverRow: null,
    filters: {},
    expandRows: [],
    defaultExpandAll: false,
    showData: null,
    isPageTable: false,
    pageSize: 10,
    currentPage: 1,
    isServerTable: false,
    selectOnIndeterminate: false
  };

  this._toggleAllSelection = debounce(10, function(states) {
    // const data = states.data || [];
    const data = states.showData || [];
    if (data.length === 0) return;
    const selection = this.states.selection;
    // when only some rows are selected (but not all), select or deselect all of them
    // depending on the value of selectOnIndeterminate
    const value = states.selectOnIndeterminate
      ? !states.isAllSelected
      : !(states.isAllSelected || selection.length);
    let selectionChanged = false;
    data.forEach((item, index) => {
      if (states.selectable) {
        if (states.selectable.call(null, item, index) && toggleRowSelection(states, item, value)) {
          selectionChanged = true;
        }
      } else {
        if (toggleRowSelection(states, item, value)) {
          selectionChanged = true;
        }
      }
    });
    const table = this.table;
    if (selectionChanged) {
      table.$emit('selection-change', selection ? selection.slice() : []);
    }
    table.$emit('select-all', selection);
    states.isAllSelected = value;
  });

  for (let prop in initialState) {
    if (initialState.hasOwnProperty(prop) && this.states.hasOwnProperty(prop)) {
      this.states[prop] = initialState[prop];
    }
  }
};

TableStore.prototype.mutations = {
  setData(states, data) {
    const dataInstanceChanged = states._data !== data;
    states._data = data;

    if (!states.isServerTable) {
      Object.keys(states.filters).forEach((columnId) => {
        const values = states.filters[columnId];
        if (!values || values.length === 0) return;
        const column = getColumnById(this.states, columnId);
        if (column && column.filterMethod) {
          data = data.filter((row) => {
            return values.some((value) => {
              return column.filterMethod.call(null, value, row);
            });
          });
        } else {
          data = data.filter((row) => {
            return values.some((value) => {
              return row[column.property].toString().toUpperCase().indexOf(value.toUpperCase()) !== -1;
            });
          });
        }
      });
    }
    Object.keys(states.filters).forEach((columnId) => {
      const values = states.filters[columnId];
      if (!values || values.length === 0) return;
      const column = getColumnById(this.states, columnId);
      if (column && column.filterMethod) {
        data = data.filter((row) => {
          return values.some(value => column.filterMethod.call(null, value, row, column));
        });
      }
    });

    states.filteredData = data;
    states.data = sortData((data || []), states);
    states.showData = showData(states.data, states);

    this.updateCurrentRow();

    const rowKey = states.rowKey;

    if (!states.reserveSelection) {
      if (dataInstanceChanged) {
        this.clearSelection();
      } else {
        this.cleanSelection();
      }
      this.updateAllSelected();
    } else {
      if (rowKey) {
        const selection = states.selection;
        const selectedMap = getKeysMap(selection, rowKey);
        const oriLength = states.selection.length;
        states.selection = [];

        // states.data.forEach((row) => {
        //   const rowId = getRowIdentity(row, rowKey);
        //   const rowInfo = selectedMap[rowId];
        //   if (rowInfo) {
        //     selection[rowInfo.index] = row;
        //   }
        // });
        states.showData.forEach((row) => {
          const rowId = getRowIdentity(row, rowKey);
          const rowInfo = selectedMap[rowId];
          if (rowInfo) {
            states.selection.push(row);
          }
        });

        if (oriLength !== states.selection.length) {
          this.table.$emit('selection-change', states.selection);
        }
        this.updateAllSelected();
      } else {
        console.warn('WARN: rowKey is required when reserve-selection is enabled.');
      }
    }

    const defaultExpandAll = states.defaultExpandAll;
    if (defaultExpandAll) {
      // this.states.expandRows = (states.data || []).slice(0);
      this.states.expandRows = (states.showData || []).slice(0);
    } else if (rowKey) {
      // update expandRows to new rows according to rowKey
      const ids = getKeysMap(this.states.expandRows, rowKey);
      let expandRows = [];
      for (const row of states.data) {
        const rowId = getRowIdentity(row, rowKey);
        if (ids[rowId]) {
          expandRows.push(row);
        }
      }
      this.states.expandRows = expandRows;
    } else {
      // clear the old rows
      this.states.expandRows = [];
    }
    if (!states.isServerTable && Object.keys(states.filters).length > 0) {
      this.table.$emit('filter-change', states.filters);
    }

    Vue.nextTick(() => this.table.updateScrollY());
  },
  pageChanged(states, obj) {

    this.clearSelection();
    states.pageSize = obj.pageSize;
    states.currentPage = obj.currentPage;
    states.showData = showData(states.data, states);

    this.updateCurrentRow();

    if (!states.reserveSelection) {
      this.cleanSelection();
      this.updateAllSelected();
    } else {
      (() => {
        const rowKey = states.rowKey;
        if (rowKey) {
          (() => {
            const selection = states.selection;
            const selectedMap = getKeysMap(selection, rowKey);

            states.showData.forEach((row) => {
              const rowId = getRowIdentity(row, rowKey);
              const rowInfo = selectedMap[rowId];
              if (rowInfo) {
                selection[rowInfo.index] = row;
              }
            });

            this.updateAllSelected();
          })();
        } else {
          console.warn('WARN: rowKey is required when reserve-selection is enabled.');
        }
      })();
    }

    const defaultExpandAll = states.defaultExpandAll;
    if (defaultExpandAll) {
      this.states.expandRows = (states.showData || []).slice(0);
    }

    Vue.nextTick(() => {
      return this.table.updateScrollY();
    });
  },
  changeSortCondition(states, options) {
    this.clearSelection();
    states.data = sortData((states.filteredData || states._data || []), states);
    states.showData = showData(states.data, states);
    if (!options || !options.silent) {
      this.table.$emit('sort-change', {
        column: this.states.sortingColumn,
        prop: this.states.sortProp,
        order: this.states.sortOrder
      });
    }

    Vue.nextTick(() => this.table.updateScrollY());
  },

  sort(states, options) {
    const { prop, order } = options;
    if (prop) {
      states.sortProp = prop;
      states.sortOrder = order || 'ascending';
      Vue.nextTick(() => {
        for (let i = 0, length = states.columns.length; i < length; i++) {
          let column = states.columns[i];
          if (column.property === states.sortProp) {
            column.order = states.sortOrder;
            states.sortingColumn = column;
            break;
          }
        }

        if (states.sortingColumn) {
          this.commit('changeSortCondition');
        }
      });
    }
  },

  filterChange(states, options) {
    this.clearSelection();
    if (options && !Array.isArray(options)) {
      options = [options];
    }
    const filters = {};
    let silent = false;
    Object.keys(states.filters).forEach((id) => {
      const column = getColumnById(this.states, id);
      column.filteredValue = [];
    });
    states.filters = {};

    options.forEach((o) => {
      let {column, values, _silent, multi} = o;
      silent = silent || _silent;
      if (values === undefined) {
        delete states.filters[column.id];
        delete filters[column.columnKey || column.id];
        return;
      }
      if (values && !Array.isArray(values)) {
        values = [values];
      }

      if (multi) {
        column.forEach(col => {
          states.filters[col.id] = values;
          filters[col.columnKey || col.id] = values;
        });
      }
      const prop = column.property;

      if (prop) {
        states.filters[column.id] = values;
        filters[column.columnKey || column.id] = values;
        column.filteredValue = values;
      }
    });

    let data = states._data;
    if (!states.isServerTable) {
      Object.keys(states.filters).forEach((columnId) => {
        const values = states.filters[columnId];
        if (!values || values.length === 0) return;
        const column = getColumnById(this.states, columnId);
        if (column && column.filterMethod) {
          data = data.filter((row) => {
            return values.some(value => column.filterMethod.call(null, value, row, column));
          });
        } else {
          data = data.filter((row) => {
            return values.some((value) => {
              return row[column.property].toString().toUpperCase().indexOf(value.toUpperCase()) !== -1;
            });
          });
        }
      });
    }

    states.filteredData = data;
    states.data = sortData(data, states);
    states.showData = showData(states.data, states);

    if (!silent) {
      this.table.$emit('filter-change', filters);
    }

    Vue.nextTick(() => this.table.updateScrollY());
  },

  insertColumn(states, column, index, parent) {
    let array = states._columns;
    if (parent) {
      array = parent.children;
      if (!array) array = parent.children = [];
    }

    if (typeof index !== 'undefined') {
      array.splice(index, 0, column);
    } else {
      array.push(column);
    }

    if (column.type === 'selection') {
      states.selectable = column.selectable;
      states.reserveSelection = column.reserveSelection;
    }

    if (this.table.$ready) {
      this.updateColumns(); // hack for dynamics insert column
      this.scheduleLayout();
    }
  },

  removeColumn(states, column, parent) {
    // let array = states._columns;
    // if (parent) {
    //   array = parent.children;
    //   if (!array) array = parent.children = [];
    // }
    // if (array) {
    //   array.splice(array.indexOf(column), 1);
    // }

    let _columns = states._columns;
    if (_columns) {
      // clear sort condition
      if (column.property === states.sortProp) {
        this.states.sortingColumn = null;
        this.states.sortProp = null;
        this.states.sortOrder = null;
        this.commit('changeSortCondition', this.states);
      } else if (states.filters[column.id]) {
        this.commit('filterChange', {
          column: column,
          values: undefined
        });
      }
      // remove from list
      _columns.splice(_columns.indexOf(column), 1);
    }
    if (this.table.$ready) {
      this.updateColumns(); // hack for dynamics remove column
      this.scheduleLayout();
    }
  },

  setHoverRow(states, row) {
    states.hoverRow = row;
  },

  setCurrentRow(states, row) {
    const oldCurrentRow = states.currentRow;
    states.currentRow = row;

    if (oldCurrentRow !== row) {
      this.table.$emit('current-change', row, oldCurrentRow);
    }
  },

  rowSelectedChanged(states, row) {
    const changed = toggleRowSelection(states, row);
    const selection = states.selection;

    if (changed) {
      const table = this.table;
      table.$emit('selection-change', selection ? selection.slice() : []);
      table.$emit('select', selection, row);
    }

    this.updateAllSelected();
  },

  toggleAllSelection(state) {
    this._toggleAllSelection(state);
  }
};

const doFlattenColumns = (columns) => {
  const result = [];
  columns.forEach((column) => {
    if (column.children) {
      result.push.apply(result, doFlattenColumns(column.children));
    } else {
      result.push(column);
    }
  });
  return result;
};

TableStore.prototype.updateColumns = function() {
  const states = this.states;
  const _columns = states._columns || [];
  states.fixedColumns = _columns.filter((column) => column.fixed === true || column.fixed === 'left');
  states.rightFixedColumns = _columns.filter((column) => column.fixed === 'right');

  if (states.fixedColumns.length > 0 && _columns[0] && _columns[0].type === 'selection' && !_columns[0].fixed) {
    _columns[0].fixed = true;
    states.fixedColumns.unshift(_columns[0]);
  }

  const notFixedColumns = _columns.filter(column => !column.fixed);
  states.originColumns = [].concat(states.fixedColumns).concat(notFixedColumns).concat(states.rightFixedColumns);

  const leafColumns = doFlattenColumns(notFixedColumns);
  const fixedLeafColumns = doFlattenColumns(states.fixedColumns);
  const rightFixedLeafColumns = doFlattenColumns(states.rightFixedColumns);

  states.leafColumnsLength = leafColumns.length;
  states.fixedLeafColumnsLength = fixedLeafColumns.length;
  states.rightFixedLeafColumnsLength = rightFixedLeafColumns.length;

  states.columns = [].concat(fixedLeafColumns).concat(leafColumns).concat(rightFixedLeafColumns);
  states.isComplex = states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0;
};

TableStore.prototype.isSelected = function(row) {
  return (this.states.selection || []).indexOf(row) > -1;
};

TableStore.prototype.clearSelection = function() {
  const states = this.states;
  states.isAllSelected = false;
  const oldSelection = states.selection;
  if (states.selection.length) {
    states.selection = [];
  }
  if (oldSelection.length > 0) {
    this.table.$emit('selection-change', states.selection ? states.selection.slice() : []);
  }
};

TableStore.prototype.setExpandRowKeys = function(rowKeys) {
  const expandRows = [];
  const data = this.states.data;
  const rowKey = this.states.rowKey;
  if (!rowKey) throw new Error('[Table] prop row-key should not be empty.');
  const keysMap = getKeysMap(data, rowKey);
  rowKeys.forEach((key) => {
    const info = keysMap[key];
    if (info) {
      expandRows.push(info.row);
    }
  });

  this.states.expandRows = expandRows;
};

TableStore.prototype.toggleRowSelection = function(row, selected) {
  const changed = toggleRowSelection(this.states, row, selected);
  if (changed) {
    this.table.$emit('selection-change', this.states.selection ? this.states.selection.slice() : []);
  }
};

TableStore.prototype.toggleRowExpansion = function(row, expanded) {
  const changed = toggleRowExpansion(this.states, row, expanded);
  if (changed) {
    this.table.$emit('expand-change', row, this.states.expandRows);
    this.scheduleLayout();
  }
};

TableStore.prototype.isRowExpanded = function(row) {
  const { expandRows = [], rowKey } = this.states;
  if (rowKey) {
    const expandMap = getKeysMap(expandRows, rowKey);
    return !!expandMap[getRowIdentity(row, rowKey)];
  }
  return expandRows.indexOf(row) !== -1;
};

TableStore.prototype.cleanSelection = function() {
  const selection = this.states.selection || [];
  const data = this.states.data;
  const rowKey = this.states.rowKey;
  let deleted;
  if (rowKey) {
    deleted = [];
    const selectedMap = getKeysMap(selection, rowKey);
    const dataMap = getKeysMap(data, rowKey);
    for (let key in selectedMap) {
      if (selectedMap.hasOwnProperty(key) && !dataMap[key]) {
        deleted.push(selectedMap[key].row);
      }
    }
  } else {
    deleted = selection.filter((item) => {
      return data.indexOf(item) === -1;
    });
  }

  deleted.forEach((deletedItem) => {
    selection.splice(selection.indexOf(deletedItem), 1);
  });

  if (deleted.length) {
    this.table.$emit('selection-change', selection ? selection.slice() : []);
  }
};

TableStore.prototype.clearFilter = function(columnKeys) {
  const states = this.states;
  const { tableHeader, fixedTableHeader, rightFixedTableHeader } = this.table.$refs;
  let panels = {};

  if (tableHeader) panels = merge(panels, tableHeader.filterPanels);
  if (fixedTableHeader) panels = merge(panels, fixedTableHeader.filterPanels);
  if (rightFixedTableHeader) panels = merge(panels, rightFixedTableHeader.filterPanels);

  const keys = Object.keys(panels);
  if (!keys.length) return;

  if (typeof columnKeys === 'string') {
    columnKeys = [columnKeys];
  }
  if (Array.isArray(columnKeys)) {
    const columns = columnKeys.map(key => getColumnByKey(states, key));
    keys.forEach(key => {
      const column = columns.find(col => col.id === key);
      if (column) {
        panels[key].filteredValue = [];
      }
    });
    this.commit('filterChange', {
      column: columns,
      value: [],
      silent: true,
      multi: true
    });
  } else {
    keys.forEach(key => {
      panels[key].filteredValue = [];
    });

    states.filters = {};

    this.commit('filterChange', {
      column: {},
      values: [],
      silent: true
    });
  }
};

TableStore.prototype.clearSort = function() {
  const states = this.states;
  if (!states.sortingColumn) return;
  states.sortingColumn.order = null;
  states.sortProp = null;
  states.sortOrder = null;

  this.commit('changeSortCondition', {
    silent: true
  });
};

TableStore.prototype.updateAllSelected = function() {
  const states = this.states;
  const { selection, rowKey, selectable, data } = states;
  if (!data || data.length === 0) {
    states.isAllSelected = false;
    return;
  }

  let selectedMap;
  if (rowKey) {
    selectedMap = getKeysMap(states.selection, rowKey);
  }

  const isSelected = function(row) {
    if (selectedMap) {
      return !!selectedMap[getRowIdentity(row, rowKey)];
    } else {
      return selection.indexOf(row) !== -1;
    }
  };

  let isAllSelected = true;
  let selectedCount = 0;
  for (let i = 0, j = data.length; i < j; i++) {
    const item = data[i];
    const isRowSelectable = selectable && selectable.call(null, item, i);
    if (!isSelected(item)) {
      if (!selectable || isRowSelectable) {
        isAllSelected = false;
        break;
      }
    } else {
      selectedCount++;
    }
  }

  if (selectedCount === 0) isAllSelected = false;

  states.isAllSelected = isAllSelected;
};

TableStore.prototype.scheduleLayout = function(updateColumns) {
  if (updateColumns) {
    this.updateColumns();
  }
  this.table.debouncedUpdateLayout();
};

TableStore.prototype.setCurrentRowKey = function(key) {
  const states = this.states;
  const rowKey = states.rowKey;
  if (!rowKey) throw new Error('[Table] row-key should not be empty.');
  const data = states.data || [];
  const keysMap = getKeysMap(data, rowKey);
  const info = keysMap[key];
  states.currentRow = info ? info.row : null;
};

TableStore.prototype.updateCurrentRow = function() {
  const states = this.states;
  const table = this.table;
  // const data = states.data || [];
  const data = states.showData || [];
  const oldCurrentRow = states.currentRow;

  if (data.indexOf(oldCurrentRow) === -1) {
    if (states.rowKey && oldCurrentRow) {
      let newCurrentRow = null;
      for (let i = 0; i < data.length; i++) {
        const item = data[i];
        if (item && item[states.rowKey] === oldCurrentRow[states.rowKey]) {
          newCurrentRow = item;
          break;
        }
      }
      if (newCurrentRow) {
        states.currentRow = newCurrentRow;
        return;
      }
    }
    states.currentRow = null;

    if (states.currentRow !== oldCurrentRow) {
      table.$emit('current-change', null, oldCurrentRow);
    }
  }
};

TableStore.prototype.commit = function(name, ...args) {
  const mutations = this.mutations;
  if (mutations[name]) {
    mutations[name].apply(this, [this.states].concat(args));
  } else {
    throw new Error(`Action not found: ${name}`);
  }
};

export default TableStore;
