import Utils, { IUtils } from '../Utils';
import Helpers, { IHelpers } from '../Helpers';
import * as _ from 'underscore';
import { IActionsButton } from '../Actions/Buttons';

export interface IPageContentLink {
  external: boolean;
  address: string;
}

export interface IPageCellStyle {
  'background-color'?: string;
  color?: string;
  'font-family'?: string;
  'font-size'?: string;
  'font-style'?: string;
  'font-weight'?: string;
  height?: string;
  'number-format'?: string;
  'text-align'?: string;
  'text-wrap'?: string;
  width?: string;
  tbs?: string;
  rbs?: string;
  bbs?: string;
  lbs?: string;
  tbc?: string;
  rbc?: string;
  bbc?: string;
  lbc?: string;
  tbw?: string;
  rbw?: string;
  bbw?: string;
  lbw?: string;
  [key: string]: any;
}

export interface IPageCellFormatting {
  background?: string;
  color?: string;
}

export interface IPageContentCellIndex {
  row: number;
  col: number;
}

export interface IPageContentCell {
  value: string | number;
  formatted_value?: string | number;
  column_name?: string;
  default_value?: string;
  pk?: boolean;
  index?: IPageContentCellIndex;
  link?: IPageContentLink;
  style: IPageCellStyle;
  // formatting?: IPageCellStyle;
  originalStyle?: IPageCellStyle;
  dirty?: boolean;
  access?: string;
  button?: IActionsButton;
  checked?: boolean;
}

export interface IPageContent extends Array<any> {
  [index: number]: IPageContentCell[];
}

export interface IPageDeltaContentCol {
  col_index: number;
  cell_content: IPageContentCell;
}

export interface IPageDeltaContentRow {
  row_index: number;
  cols: IPageDeltaContentCol[];
}

export interface IPageDelta {
  new_rows: number[];
  new_cols: number[];
  content_delta: IPageDeltaContentRow[];
}

export interface IPageQuery {
  updates?: any[];
  deletes?: any[];
  error?: string;
}

export interface IPageCellUpdate {
  row: number;
  col: number;
  cell: IPageContentCell;
}

export interface IPageContentProvider {
  // static DEFAULT_CELL_STYLE: IPageCellStyle;
  structured: boolean;
  deltas: IPageContent;
  current: IPageContent;
  original: IPageContent;
  canDoDelta: boolean;
  dirty: boolean;
  rowLen: number;
  colLen: number;

  update: (rawContent: IPageContent, clean?: boolean, replace?: boolean) => void;
  updateDelta(content: IPageContent): void;
  reset: (content?: IPageContent) => void;
  getCellByRef: (ref: string) => IPageContentCell;
  getCell: (rowIndex: number, columnIndex: number) => IPageContentCell;
  getCells: (fromCell: IPageContentCellIndex, toCell: IPageContentCellIndex, valueOnly?: boolean) => any;
  getCellsByRange: (str: string, valueOnly?: boolean) => any;
  getRowCells: (rowIndex: number) => any;
  getColCells: (colIndex: number) => any;
  updateCell: (rowIndex: number, columnIndex: number, data: IPageContentCell, delta?: boolean) => void;
  updateCells(from: IPageContentCellIndex, to: IPageContentCellIndex, data: IPageContentCell): void;
  // insertRow: (data: any) => void;
  addRow: (index: number, direction?: string) => void;
  addColumn: (index: number, direction?: string) => void;
  removeRow: (index: number, direction?: string) => void;
  removeColumn: (index: number) => void;
  setColSize(col: number, value: number): void;
  setRowSize(row: number, value: number): void;
  getCellDeltas: () => IPageDelta;
  getQuery: () => IPageQuery;
  getDelta: (clean?: boolean) => IPageDelta;
  getFull: () => IPageContent;
  getHtml(from?: IPageContentCellIndex, to?: IPageContentCellIndex): any;
  flattenStyles(styles: IPageCellStyle, only?: string[]): string;
}
export class PageContent implements IPageContentProvider {
  public structured: boolean = false;
  public canDoDelta: boolean = true;
  public dirty: boolean = false;

  private _original: IPageContent = [[]];
  private _current: IPageContent = [[]];
  private _deltas: IPageContent = [];

  private _newRows: number[] = [];
  private _newCols: number[] = [];

  private _utils: IUtils;
  private _helpers: IHelpers;

  // private _defaultStyles: IPageCellStyle = {
  //   'background-color': 'FFFFFF',
  //   color: '000000',
  //   'font-family': 'sans-serif',
  //   'font-size': '11pt',
  //   'font-style': 'normal',
  //   'font-weight': 'normal',
  //   height: '20px',
  //   'number-format': '',
  //   'text-align': 'left',
  //   'text-wrap': 'normal',
  //   width: '64px',
  //   tbs: 'none',
  //   rbs: 'none',
  //   bbs: 'none',
  //   lbs: 'none',
  //   tbc: '000000',
  //   rbc: '000000',
  //   bbc: '000000',
  //   lbc: '000000',
  //   tbw: 'none',
  //   rbw: 'none',
  //   bbw: 'none',
  //   lbw: 'none',
  // };

  private borders: any = {
    widths: {
      none: 0,
      thin: 1,
      medium: 2,
      thick: 3
    },
    styles: {
      none: 'solid',
      solid: 'solid',
      double: 'double'
    },
    names: {
      t: 'top',
      r: 'right',
      b: 'bottom',
      l: 'left'
    }
  };

  private cellStyles: string[] = [
    'background-color',
    'color',
    'font-family',
    'font-size',
    'font-style',
    'font-weight',
    'height',
    'text-align',
    'text-wrap',
    'width',
    'vertical-align'
  ];

  public get deltas(): IPageContent {
    return this._deltas;
  }

  public get current(): IPageContent {
    return this._current;
  }

  public get original(): IPageContent {
    return this._original;
  }

  constructor(rawContent: IPageContent) {
    if (!rawContent || rawContent.constructor !== Array || !rawContent.length || !rawContent[0].length) {
      rawContent = this.defaultContent();
    }
    this._utils = new Utils();
    this._helpers = new Helpers();
    // this._original = this._utils.clonePageContent(rawContent);
    // this._current = this.mergeStyles(rawContent);
    this.update(rawContent, true);
  }

  public update(rawContent: IPageContent, clean: boolean = false, replace: boolean = false): void {
    // page history
    if (replace) {
      this.dirty = true;
      this._current = rawContent;
      this._deltas = [];
      return;
    } else {
      this._original = rawContent;
    }    
    let style: IPageCellStyle = { ...this._utils.getDefaultCellStyle() };
    this._current = this._utils.mergePageContent(this._original, clean ? null : this._deltas, style);
  }

  public updateDelta(content: IPageContent): void {
    if (!content || !content.length) return;
    for (let rowIndex = 0; rowIndex < content.length; rowIndex++) {
      let row = content[rowIndex];
      if (!row) continue;
      for (let colIndex = 0; colIndex < row.length; colIndex++) {
        let col = row[colIndex];
        if (!col || !this._current[rowIndex] || !this._current[rowIndex][colIndex]) continue;
        this._original[rowIndex][colIndex] = col;
        this._current[rowIndex][colIndex] = col;
      }
    }    
  }

  public reset(content?: IPageContent): void {
    // Remove new rows and columns
    for (let i: number = 0; i < this._newRows.length; i++) {
      this._current.splice(this._newRows[i], 1);
    }

    for (let i: number = 0; i < this._newCols.length; i++) {
      for (let j: number = 0; j < this._current.length; j++) {
        this._current[j].splice(this._newCols[i], 1);
      }
    }

    this._deltas = [];
    this.dirty = false;
    this.update(content || this._original, true);
  }

  public get rowLen(): number {
    return this.current.length;
  }
  public get colLen(): number {
    return this.current.length ? this.current[0].length : 0;
  }

  public getCellByRef(ref: string): IPageContentCell {
    let rowCol = this._helpers.getRefIndex(ref);
    return this.getCell(rowCol[0], rowCol[1]);
  }

  public getCell(rowIndex: number, columnIndex: number): IPageContentCell {
    if (!this._current[rowIndex]) {
      throw new Error('row out of bounds');
    }

    if (!this._current[rowIndex][columnIndex]) {
      throw new Error('column out of bounds');
    }

    return this._current[rowIndex][columnIndex];
  }

  public getCells(fromCell: any, toCell: any, valueOnly?: boolean, noIndex?: boolean): IPageContent {
    let cells: any = [];
    let rowIndex = 0;
    let colIndex = 0;
    for (let row = fromCell.row; row <= toCell.row; row++) {
      colIndex = 0;
      if (noIndex) cells[rowIndex] = [];
      for (let col = fromCell.col; col <= toCell.col; col++) {
        if (!cells[row] && !noIndex) cells[row] = [];
        cells[noIndex ? rowIndex : row][ noIndex ? colIndex : col] = valueOnly ? this._current[row][col].value : this._current[row][col];
        colIndex++;
      }
      rowIndex++;
    }
    return cells;
  }

  public getCellsByRange(str: string, valueOnly?: boolean, noIndex?: boolean): any {
    let range = this._helpers.cellRange(str, this._current.length, this._current[0].length);
    return this.getCells(range.from, range.to, valueOnly, noIndex);
  }

  public getRowCells(rowIndex: number) {
    if (!this._current[rowIndex]) {
      throw new Error('row out of bounds');
    }
    return this._current[rowIndex];
  }

  public getColCells(columnIndex: number) {
    if (!this._current[0][columnIndex]) {
      throw new Error('column out of bounds');
    }
    let cells: IPageContentCell[] = []; 
    for (let row = 0; row < this._current.length - 1; row++) {
      cells.push(this._current[row][columnIndex]);
    }    
    return cells;
  }

  public updateCellByRef(ref: string, data: IPageContentCell): void {
    let rowCol = this._helpers.getRefIndex(ref);
    this.updateCell(rowCol[0], rowCol[1], data);
  }

  public updateCell(rowIndex: number, columnIndex: number, data: IPageContentCell, delta: Boolean = true): void {
    if (!this._current[rowIndex]) {
      throw new Error('row out of bounds');
    }

    if (!this._current[rowIndex][columnIndex]) {
      throw new Error('column out of bounds');
    }

    // Check if cell has been edited
    // data.dirty = true;
    // this.dirty = true;

    // @todo This will probably not work with styles
    let cell: any = { ...this._current[rowIndex][columnIndex], ...data };
    if (data.style) {
      // delete data.style.width;
      // delete data.style.height;
      cell.style = { ...this._current[rowIndex][columnIndex].style, ...data.style };
      cell.originalStyle = { ...this._current[rowIndex][columnIndex].style };
    }
    this._current[rowIndex][columnIndex] = cell;
    if (delta) this.addDelta(cell, rowIndex, columnIndex);
  }

  public updateCells(from: IPageContentCellIndex, to: IPageContentCellIndex, data: IPageContentCell): void {
    for (let rowIndex = from.row; rowIndex <= to.row; rowIndex++) {
      for (let colIndex = from.col; colIndex <= to.col; colIndex++) {
        this.updateCell(rowIndex, colIndex, data);
      }
    }
  }

  // public insertRow (data: any): void {

  // }

  public addRow(index: number, direction?: string): void {
    if (!this._current[index]) {
      index = this._current.length - 1;
      direction = 'below';
    }
    let inc: number = direction === 'below' ? 1 : 0;
    let newRowData: IPageContentCell[] = [];

    // Clone row before
    if (this._current.length) {
      for (let i: number = 0; i < this._current[index].length; i++) {
        let clone = JSON.parse(JSON.stringify(this._current[index][i]));
        clone.value = '';
        clone.formatted_value = '';
        clone.dirty = true;
        if(clone.link) delete clone.link;
        newRowData.push(clone);
      }
    }

    this._current.splice(index + inc, 0, newRowData);
    this._current = this._utils.mergePageContent(this._current);
  }

  public moveColumns(fromIndex: number, toIndex: number, newIndex: number): void {
    if (!this._current[0]) {
      throw new Error('row out of bounds');
    }
    if (!this._current[0][fromIndex]) {
      throw new Error('column out of bounds');
    }
    if (!this._current[0][newIndex]) {
      throw new Error('column out of bounds');
    }
    // if (newIndex > fromIndex) {
    //     newIndex++;
    // }
    // let cells: IPageContentCell[] = [];
    // get columns cells
    for (let i: number = 0; i < this._current.length; i++) {
      let clone: IPageContentCell;
      let cells: IPageContentCell[] = [];
      for (let k: number = 0; k < this._current[i].length; k++) {
        if (k < fromIndex || k > toIndex) {
          continue;
        }
        clone = JSON.parse(JSON.stringify(this._current[i][k]));
        clone.dirty = true;
        cells.push(clone);
      }
      let cellStr: string[] = [];
      for (let n: number = 0; n < cells.length; n++) {
        cellStr.push(`cells[${n}]`);
      }
      let evalStr = `this._current[i].splice(newIndex, 0, ${cellStr.join(',')});`;
      eval(evalStr);
      if (newIndex > fromIndex) {
        this._current[i].splice(fromIndex, 1 + toIndex - fromIndex);
      } else {
        this._current[i].splice(fromIndex + 1 + toIndex - fromIndex, 1 + toIndex - fromIndex);
      }
    }
    this._current = this._utils.mergePageContent(this._current);
  }

  public addColumn(index: number, direction?: string): void {
    if (!this._current[0][index]) {
      index = this._current[0].length - 1;
      direction = 'right';
    }
    let inc: number = direction === 'right' ? 1 : 0;
    for (let i: number = 0; i < this._current.length; i++) {
      let clone;
      // for (let k: number = 0; k < this._current[i].length; k++) {
      //     if (k !== index) {
      //         continue;
      //     }
      clone = JSON.parse(JSON.stringify(this._current[i][index]));
      clone.dirty = true;
      clone.value = '';
      clone.formatted_value = '';
      if(clone.link) delete clone.link;
      // clone.index.col = index + inc;
      // }
      // this._current[i][index].index.col += direction === "right" ? 0 : 1;
      this._current[i].splice(index + inc, 0, clone);
    }
    this._current = this._utils.mergePageContent(this._current);
  }

  public removeRow(index: number): void {
    if (!this._current[index] || this._current.length === 1) {
      return;
    }
    this._current.splice(index, 1);
    this._current = this._utils.mergePageContent(this._current);
    if (this._deltas[index]) {
      this._deltas[index] = [];
    }
  }

  public removeColumn(index: number): void {
    if (!this._current[0][index] || this._current[0].length === 1) {
      return;
    }
    for (let i: number = 0; i < this._current.length; i++) {
      this._current[i].splice(index, 1);
      if (this._deltas[i] && this._deltas[i][index]) {
        delete this._deltas[i][index];
      }
    }
    this._current = this._utils.mergePageContent(this._current);
  }

  public setRowSize(row: number, value: number): void {
    if (!this._current[row]) {
      return;
    }
    for (let colIndex: number = 0; colIndex < this._current[row].length; colIndex++) {
      let cell: IPageContentCell = this._current[row][colIndex];
      this.updateCell(row, colIndex, { value: cell.value, formatted_value: cell.formatted_value, style: { height: `${value}px` } }, true);
      // if (!cell.style) cell.style = {};
      // cell.style.height = `${value}px`;
      // cell.dirty = true;
    }
  }
  public setColSize(col: number, value: number): void {
    if (!this._current[0][col]) {
      return;
    }
    for (let rowIndex: number = 0; rowIndex < this._current.length; rowIndex++) {
      let cell: IPageContentCell = this._current[rowIndex][col];
      this.updateCell(rowIndex, col, { value: cell.value, formatted_value: cell.formatted_value, style: { width: `${value}px` } }, true);
      // if (!cell.style) cell.style = {};
      // cell.style.width = `${value}px`;
      // cell.dirty = true;
    }
  }

  // @todo Would be better to supply the two contents rather than using "this"
  public getDelta(): IPageDelta {
    // This is expensive, but will get proper styles
    let current: IPageContent = this._utils.clonePageContent(this._current);

    /**
     * Specs (Jira IPPWSTWO-195)
     * @type {{new_rows: Array, new_cols: Array, content_delta: *[]}}
     */
    let deltaStructure: IPageDelta = {
      // First "commit" changes to the layout of page - add new cols and rows
      new_rows: [], // List of new rows. List through all of them and add+1 to all after @todo: to have true delta, we should have extra endpoint to add row
      new_cols: [], // List of new cols. List through all of them and add+1 to all after @todo: to have true delta, we should have extra endpoint to add column

      // @todo: Handle removed rows and cols

      // Second, process the data - Data will have right referencing already with added rows and cells
      content_delta: [
        {
          row_index: 0,
          cols: [
            {
              col_index: 0,
              cell_content: {
                value: '',
                style: {}
              }
            }
          ]
        }
      ]
    };

    /**
     *  --------------------------------------------
     */

    deltaStructure.content_delta = []; // just empty it
    deltaStructure.new_rows = this._newRows;
    deltaStructure.new_cols = this._newCols;

    let rowMovedBy: number = 0;
    let colMovedBy: number = 0;

    // @todo Every cell will get full styles basically braking inheritence model = adding more data to a page. We need to be able to work around this somehow
    for (let i: number = 0; i < current.length; i++) {
      let rowData: any = {};
      let newRow: boolean = this._newRows.indexOf(i) >= 0;

      colMovedBy = 0;

      if (newRow) {
        rowData = {
          row_index: i,
          cols: []
        };

        rowMovedBy++;
      }

      for (let j: number = 0; j < current[i].length; j++) {
        if (newRow) {
          let cell: IPageContentCell = _.clone(current[i][j]);
          delete cell.dirty;
          // delete cell.formatting;

          rowData.cols.push({
            col_index: j,
            cell_content: cell
          });
        } else {
          let newCol: boolean = this._newCols.indexOf(j) >= 0;

          if (newCol) {
            colMovedBy++;
          }

          if (newCol || current[i][j].dirty) {
            if (!Object.keys(rowData).length) {
              rowData = {
                row_index: i,
                cols: []
              };
            }

            let cell: IPageContentCell = _.clone(current[i][j]);
            delete cell.dirty;
            delete cell.originalStyle;
            delete cell.button;

            rowData.cols.push({
              col_index: j,
              cell_content: cell
            });
          }
        }
      }

      if (Object.keys(rowData).length) {
        deltaStructure.content_delta.push(rowData);
      }

      // Sort new cols and rows
      // @todo uncomment this when deleting rows and columns is enabled
      /*deltaStructure.new_cols.sort(ipp.this._utils.sortByNumber);
           deltaStructure.new_rows.sort(ipp.this._utils.sortByNumber);*/
    }

    // @todo Do something with styles

    return deltaStructure;
  }

  public getCellDeltas(): IPageDelta {
    let deltaStructure: IPageDelta = {
      new_rows: [],
      new_cols: [],
      content_delta: [
        {
          row_index: 0,
          cols: [
            {
              col_index: 0,
              cell_content: {
                value: '',
                style: {}
              }
            }
          ]
        }
      ]
    };
    deltaStructure.content_delta = [];
    let deltas: IPageContent[] = this._deltas.concat();
    this._deltas = [];

    for (let rowIndex = 0; rowIndex < deltas.length; rowIndex++) {
      let row = deltas[rowIndex];
      if (!row) continue;
      let cols: IPageDeltaContentCol[] = [];
      for (let colIndex = 0; colIndex < row.length; colIndex++) {
        let col = row[colIndex];
        if (!col) continue;

        let cell: IPageContentCell = _.clone(this._current[rowIndex][colIndex]);
        delete cell.dirty;
        delete cell.originalStyle;
        delete cell.button;

        cols.push({
          col_index: colIndex,
          cell_content: cell
        });
      }
      // if (hasSchema && rowIndex - 1 < 0) continue;
      deltaStructure.content_delta.push({
        row_index: rowIndex,
        cols: cols
      });
    }
    return deltaStructure;
  }

  public getQuery(): IPageQuery {

    let deltas: IPageContent[] = this._deltas.concat();
    this._deltas = [];   
    
    // columns and pks
    let columns: any = [];
    let pks: any = [];
    this.current[0].forEach((cell, colIndex) => {
      if (cell.pk) pks.push(colIndex);
      columns.push(cell.column_name);
    });
    if (!pks.length) {
      return {
        error: 'No primary keys found'
      };
    }

    let updates: any = [];

    for (let rowIndex = 0; rowIndex < deltas.length; rowIndex++) {
      let row = deltas[rowIndex];
      if (!row) continue;
      let update: any = {};
      let where: any = {};
      for (let colIndex = 0; colIndex < row.length; colIndex++) {
        let col = row[colIndex];
        let cell: IPageContentCell = this._current[rowIndex][colIndex];
        let ogCell: IPageContentCell = this._original[rowIndex][colIndex];
        if (pks.includes(colIndex)) {
          where[columns[colIndex]] = ogCell.value;
        }
        if (!col) continue;
        update[columns[colIndex]] = cell.value;
      }
      updates.push({
        update,
        where
      });
    }

    /*{
      "updates":[
         {
            "update":{
               "bid":100.123,
               "ask":100.125
            },
            "where":{
               "isin":"JIMBO12345"
            }
         },
      ],
      "deletes":[
         {
            "where":{
               "country":"France"
            }
         }
      ]
   }*/
   let query: IPageQuery = {};
   if (updates.length) query.updates = updates;
   return query;
  }

  public getFull(): IPageContent {
    let content: IPageContent = this._utils.clonePageContent(this._current);

    // Remove dirty indicator
    for (let i: number = 0; i < content.length; i++) {
      for (let j: number = 0; j < content[i].length; j++) {
        delete content[i][j].dirty;
        delete content[i][j].originalStyle;
        // delete content[i][j].formatting;
      }
    }

    return content;
  }

  public getHtml(from?: IPageContentCellIndex, to?: IPageContentCellIndex): any {
    let left: number = 0;
    let width: number = 0;
    let height: number = 0;
    let cells: any[] = [];
    let html = `<table style="border-collapse: collapse;">`;
    for (let rowIndex = 0; rowIndex < this._current.length; rowIndex++) {
      cells[rowIndex] = [];
      let row = this._current[rowIndex];
      html += `<tr>`;
      left = 0;
      for (let colIndex = 0; colIndex < row.length; colIndex++) {
        let cell: any = row[colIndex];
        let styles = this.flattenStyles(cell.style);
        html += `<td style="${styles}"><div>${cell.formatted_value}</div></td>`;
        left += parseFloat(cell.style.width);
        if (!rowIndex) {
          width += parseFloat(cell.style.width);
        }
        if (!colIndex) {
          height += parseFloat(cell.style.height);
        }
        cells[rowIndex].push({
          value: cell.formatted_value,
          style: styles
        });
      }
      let colLast: IPageContentCell = row[row.length - 1];
      if (!rowIndex && colLast.style && colLast.style.rbw) {
        // width += parseFloat(colLast.style.rbw);
      }
      html += `</tr>`;
    }
    let cellLast: IPageContentCell = this._current[this._current.length - 1][0];
    if (cellLast.style && cellLast.style.rbw && cellLast.style.rbw !== 'none') {
      // height += parseFloat(cellLast.style.rbw);
    }
    html += `</table>`;
    return {
      width: width,
      height: height,
      html: html,
      cells
    };
  }

  public flattenStyles(styles: IPageCellStyle, only?: string[]): string {
    let htmlStyle: any = {};

    this.cellStyles.forEach(s => {
      htmlStyle[s] = styles[s];
    });

    for (let key in this.borders.names) {
      let name: string = this.borders.names[key];
      let width: string = this.borders.widths[styles[`${key}bw`]] + 'px';
      let style: string = styles[`${key}bs`];
      let color: string = '#' + styles[`${key}bc`].replace('#', '');
      htmlStyle[`border-${name}`] = `${width} ${style} ${color}`;
    }

    let str = '';
    for (let attr in htmlStyle) {
      let value = htmlStyle[attr];
      if ((attr === 'background-color' || attr === 'color') && value.indexOf('#') === -1) {
        value = `#${value}`;
      }
      if (only && only.indexOf(attr) < 0) continue;
      str += `${attr}:${value};`;
    }
    return str;
  }

  private addDelta(cell: IPageContentCell, row: number, col: number): void {
    if (!this._deltas[row]) this._deltas[row] = [];
    this._deltas[row][col] = cell;
  }

  private defaultContent(): any {
    return [
      [
        {
          value: '',
          formatted_value: ''
        }
      ]
    ];
  }

  // private getColumns(): any {
  //   // columns and pks
  //   let columns: any = [];
  //   let pks: any = [];
  //   this.current[0].forEach((cell, colIndex) => {
  //     if (cell.pk) pks.push(colIndex);
  //     columns.push(cell.column_name);
  //   });
  //   if (!pks.length) {
  //     return {
  //       error: 'No primary keys found'
  //     };
  //   }    
  //   return {
  //     pks,
  //     columns
  //   }
  // }
}
