import Emitter from './Emitter';
import Helpers, { IHelpers } from './Helpers';
import _ from 'underscore';
import merge from 'deepmerge';
import Utils, { IUtils } from './Utils';

export interface IClipboard {
  ON_DATA: string;
  ON_PASTED: string;
  init(element: any, doc: boolean): boolean;
  copyTextToClipboard(text: string, el?: string): boolean;
  destroy(): void;
}

export class Clipboard extends Emitter implements IClipboard {
  // public static $inject: string[] = [
  //     "$timeout",
  //     "Helpers",
  // ];

  // events
  public get ON_DATA(): string {
    return 'data';
  }
  public get ON_PASTED(): string {
    return 'pasted';
  }

  public editableAreaId: string = 'pages-editable';
  public editableAreaEl: any;
  public focus: boolean = false;
  public clipboardTextPlain: string = '';

  private key: string = '';
  private doc: boolean = false;

  private validStyles: any = [
    'color',
    'background',
    'background-color',
    'border',
    'border-left',
    'border-right',
    'border-top',
    'border-bottom',
    'width',
    'height',
    'text-align',
    'vertical-align',
    'font-family',
    'font-size',
    'font-weight',
    'font-style',
    'text-wrap',
    'text-decoration',
    'number-format',
    'word-wrap',
    'white-space'
  ];

  /**
   * Linking names of excel/json styles to css styles
   */
  private excelStyles: any = ['tb', 'rb', 'bb', 'lb'];

  /**
   * Map excel border styles to css border styles (with some compromise)
   */
  private excelBorderStyles: any = {
    solid: 'solid',
    dashed: 'dashed',
    dashdotdot: 'dotted',
    double: 'double'
  };

  /**
   * Map excel border weights to css border weights (with some compromise)
   */
  private excelBorderWeights: any = {
    '1px': 'thin',
    '2px': 'medium',
    '3px': 'thick'
  };

  private borderSides: any = ['top', 'right', 'bottom', 'left'];
  private borderSyles: any = ['width', 'style', 'color'];
  private helpers: IHelpers;
  private utils: IUtils;
  private canvas!: HTMLCanvasElement;

  constructor(element?: any, doc?: boolean) {
    super();
    this.helpers = new Helpers();
    this.utils = new Utils();
    this.init(element, doc);
    return;
  }

  public init(element?: any, doc?: boolean): boolean {
    this.destroy();

    if (typeof element === 'string') {
      this.editableAreaEl = document.getElementById(element);
    } else {
      this.editableAreaEl = element;
    }

    if (this.editableAreaEl) {
      this.helpers.addEvent(this.editableAreaEl, 'paste', this.onPaste);
      this.helpers.addEvent(this.editableAreaEl, 'focus', this.onEditFocus);
      this.helpers.addEvent(this.editableAreaEl, 'blur', this.onEditBlur);
      this.helpers.addEvent(this.editableAreaEl, 'keypress', this.onKeyDown);
      this.helpers.addEvent(this.editableAreaEl, 'drop', this.onPaste);
      this.editableAreaEl.focus();
    }

    if (doc) {
      this.helpers.addEvent(document, 'paste', this.onPasteDocument);
      this.doc = true;
    } else {
      this.doc = false;
    }

    return true;
  }

  public destroy(): void {
    this.focus = false;
    if (this.editableAreaEl) {
      this.helpers.removeEvent(this.editableAreaEl, 'paste', this.onPaste);
      this.helpers.removeEvent(this.editableAreaEl, 'focus', this.onEditFocus);
      this.helpers.removeEvent(this.editableAreaEl, 'blur', this.onEditBlur);
      this.helpers.removeEvent(this.editableAreaEl, 'keypress', this.onKeyDown);
      this.helpers.removeEvent(this.editableAreaEl, 'drop', this.onPaste);
    }
    if (this.doc) {
      this.helpers.removeEvent(document, 'paste', this.onPasteDocument);
    }
    let pasted: any = document.getElementById('pasted');
    if (pasted) {
      pasted.parentElement.removeChild(pasted);
    }
  }

  public copyTextToClipboard(text: string, el: string = 'table'): boolean {
    let div = document.createElement('div');
    let custom = el ? true : false;
    div.classList.add('clipboard');
    div.style.position = 'fixed';
    div.style.bottom = '100vh';
    div.style.right = '100vw';
    if (el) {
      div.innerHTML = text;
    } else {
      el = 'textarea';
      div.innerHTML = `<textarea>${text}</textarea>`;
      // input = document.createElement('textarea');
      // input.value = text;
      // div.appendChild(input);
    }

    document.body.appendChild(div);
    let success: boolean = true;

    try {
      let firstEl = div.getElementsByTagName(el)[0] as any;
      if (custom) {
        this.selectElContents(firstEl);
      } else {
        firstEl.select();
        firstEl.setSelectionRange(0, 99999);
      }
      success = document.execCommand('copy');
    } catch (err) {
      success = false;
    }

    document.body.removeChild(div);

    return success;
  }

  // @todo Should probably not be here
  // @note Taken from http://stackoverflow.com/a/2044793/820942
  private selectElContents(el: any): void {
    let body: any = document.body,
      range,
      sel;
    if (document.createRange && window.getSelection) {
      range = document.createRange();
      sel = window.getSelection();
      if (!sel) {
        throw 'Error';
      }
      try {
        sel.removeAllRanges();
        range.selectNodeContents(el);
        sel.addRange(range);
      } catch (e) {
        try {
          range.selectNode(el);
          sel.addRange(range);
        } catch (e) {
          throw e;
        }
      }
    } else if (body.createTextRange) {
      range = body.createTextRange();
      range.moveToElementText(el);
      range.select();
    }
  }

  private onEditFocus: any = () => {
    this.focus = true;
  };

  private onEditBlur: any = () => {
    this.focus = false;
  };

  private onPasteDocument: any = (e: any) => {
    if (this.focus) {
      return;
    }
    this.emit(this.ON_PASTED);
    let clipboard: any = this.getClipboardText(e);
    this.clipboardTextPlain = clipboard.text;
    let div: any = this.createPastedElement(clipboard.html || clipboard.text);
    this.getHtml(div);
    console.log('onPasteDocument', clipboard);
  };

  private getClipboardText(e: any): any {
    let clipboard: any = {
      html: '',
      text: ''
    };
    try {
      // Other
      clipboard.text = e.clipboardData.getData('text/plain');
      clipboard.html = e.clipboardData.getData('text/html');
    } catch (exception) {}
    // try {
    //   // IE
    //   if (!clipboard.text) {
    //     clipboard.text = window.clipboardData.getData('Text');
    //   }
    // } catch (exception) {}
    return clipboard;
  }

  private onPaste: any = (e: any) => {
    console.log('onPaste', e);
    this.emit(this.ON_PASTED);

    // try and get plain text input
    let clipboard: any = this.getClipboardText(e);
    this.clipboardTextPlain = clipboard.text;

    // try and get html
    try {
      let div: any = this.createPastedElement(clipboard.html);
      this.getHtml(div);
      e.preventDefault();
    } catch (exception) {
      this.getHtml(e.target);
    }

    console.log('onPaste', clipboard);
  };

  private createPastedElement(html: string): any {
    let div: any = document.getElementById('pasted') || document.createElement('div');
    div.id = 'pasted';
    if (html.indexOf('<img') > -1 || html.indexOf('<script') > -1) {
      html = '';
    }
    div.innerHTML = html;
    div.style.left = '-10000px';
    div.style.top = '-10000px';
    div.style.position = 'absolute';
    div.style['z-index'] = -1;
    document.getElementsByTagName('body')[0].appendChild(div);
    return div;
  }

  private getHtml(el: any): void {
    setTimeout(() => {
      let tables: any = el.getElementsByTagName('table');

      // check for table
      if (!tables.length) {
        el.innerHTML = '';
        if (!this.clipboardTextPlain) {
          this.emit(this.ON_DATA, false);
        } else {
          // just use what we got
          console.log(this.clipboardTextPlain);
          let data: any = this.parseText(this.clipboardTextPlain);
          this.emit(this.ON_DATA, data);
        }
        return;
      }

      this.parseTable(tables[0]);
      el.style.visibility = 'hidden';
    }, 10);
  }

  private onKeyDown: any = (e: KeyboardEvent) => {
    this.key = e.key;
    if (this.key === 'Control' && e.key === 'v') {
      this.emit(this.ON_PASTED);
    } else {
      // e.preventDefault();
    }
  };

  private parseText(text: string): any {
    let data: any = [];
    let lines: any;
    let colWidths: any = [];
    let numOfCols: number = 0;
    let rows: any = [];

    try {
      let jsonData = JSON.parse(text);
      if (!jsonData.length) {
        throw 'Not an array';
      }
      let firstRow = jsonData[0];
      let cells: any = [];
      Object.keys(firstRow).forEach(key => {
        if (typeof key !== 'string' && typeof key !== 'number') {
          key = JSON.stringify(key);
        }
        cells.push(key);
      });
      numOfCols = cells.length;
      rows.push(cells);
      jsonData.forEach((row: any) => {
        let cells: any = [];
        Object.keys(row).forEach((key, c) => {
          let value = row[key];
          if (typeof value !== 'string' && typeof value !== 'number') {
            value = JSON.stringify(value);
          }
          cells.push(value);
          let width: number = Math.ceil(this.getTextWidth(row[key], 'normal 12pt Arial'));
          if (!colWidths[c] || colWidths[c] < width) {
            colWidths[c] = width;
          }
        });
        rows.push(cells);
      });
    } catch (e) {
      // TOOD: use a proper library to do this
      lines = text.split('\n');

      let tabs = lines[0].match(/\t/gi) ? lines[0].match(/\t/gi).length : 0;
      let commas = lines[0].match(/,/gi) ? lines[0].match(/,/gi).length : 0;
      for (let i: number = 0; i < lines.length; i++) {
        let line: string = lines[i]; // .replace(/\t([^\s])/ig, ",$1");
        let cells: any;
        if (tabs >= commas) {
          cells = line.split(/\t(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))/);
        } else {
          cells = line.split(/,(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))/);
        }
        if (cells.length < 2) {
          line = line.replace(/( {2,9})/gi, ',');
          cells = line.split(/,(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))/);
        }
        rows[i] = cells;
        if (cells.length > numOfCols) {
          numOfCols = cells.length;
        }
        for (let c: number = 0; c < cells.length; c++) {
          let width: number = Math.ceil(this.getTextWidth(cells[c], 'normal 12pt Arial'));
          if (!colWidths[c] || colWidths[c] < width) {
            colWidths[c] = width;
          }
        }
      }
    }

    for (let i: number = 0; i < rows.length; i++) {
      let cells: any = rows[i];
      let cols: any = [];
      for (let c: number = 0; c < numOfCols; c++) {
        let value: any = cells[c] ? cells[c].replace(/['"]+/g, '') : '';
        let textAlign: string = /^\d+$/.test(value) ? 'right' : 'left';
        let style = this.utils.getDefaultCellStyle();
        style.width = `${colWidths[c]}px`;
        style["text-align"] = textAlign;
        cols.push({
          style,
          formatted_value: value,
          value: value,
          index: {
            row: i,
            col: c
          }
        });
      }
      data.push(cols);
    }
    return data;
  }

  private parseTable(table: any): void {
    let data: any = this.collectClipData(table);
    this.emit(this.ON_DATA, data);
  }

  private collectClipData(table: any): any {
    let data: any = [];
    let colWidths: any = this.tableColWidths(table);
    let rows: any = table.rows;
    let maxColumns: number = (colWidths && colWidths.length) || 0;
    let rowCount: number = 0;
    let rowSpans: any = [];

    // get max columns
    if (!colWidths || !colWidths.length) {
      colWidths = [];
      let rowIndex = -1;
      for (let i: number = 0; i < rows.length; i++) {
        let cells: any = rows[i].cells;
        if (!cells.length) {
          continue;
        }
        let cellCount = 0;
        for (let j: number = 0; j < cells.length; j++) {
          cellCount += cells[j].colSpan;
        }
        if (cellCount > maxColumns) {
          maxColumns = cellCount;
          rowIndex = i;
        }
      }
      for (let j = 0; j < rows[rowIndex].cells.length; j++) {
        let style: any = this.cssToStyles(window.getComputedStyle(rows[rowIndex].cells[j]).cssText);
        colWidths[j] = parseFloat(style.width) != NaN ? parseFloat(style.width) : 120;
      }
    }
    console.log('colwidths', colWidths);

    for (let i: number = 0; i < rows.length; i++) {
      let cells: any = rows[i].cells;

      // just ignore the row if no cells found
      if (!cells.length) {
        continue;
      }

      data[rowCount] = [];
      let cellCount: number = 0;
      let rowHeight: string = '';

      // add row spaning cells first
      if (rowSpans[rowCount]) {
        for (let rs = 0; rs < rowSpans[rowCount].length; rs++) {
          let c: any = rowSpans[rowCount][rs];
          if (!c) continue;
          data[rowCount][c.index.col] = c;
          // cellCount++;
          if (!rowHeight) rowHeight = c.style.height;
        }
      }

      // main cell creation loop
      for (let j: number = 0; j < maxColumns; j++) {
        // check for row span cell and move cell count pointer
        if (rowSpans[rowCount] && rowSpans[rowCount][cellCount]) {
          // cellCount++;
          for (let s = cellCount; s < maxColumns; s++) {
            if (!rowSpans[rowCount][s]) break;
            cellCount++;
          }
        }

        if (!cells[j]) {
          continue;
        }

        let rowSpan: number = cells[j].rowSpan;
        let colSpan: number = cells[j].colSpan;
        let links: any = cells[j].getElementsByTagName('a');

        // get value
        let val: any = cells[j].textContent;
        if (links && links.length) {
          val = links[0].textContent;
        }
        val = val
          // .replace(/<(?!br\s*\/?)[^>]+>/g, '')
          .replace(/\n/g, '')
          .replace(/<br>/g, '\n')
          .replace(/<br \/>/g, '\n')
          // .replace(/&amp;/g, '&')
          // .replace(/&lt;/g, '<')
          // .replace(/&nbsp;/g, ' ')
        val = val.split('\n').map((v: string) => v.trim()).join('\n');

        // cell data
        let cellData: any = {
          index: {
            row: rowCount,
            col: cellCount
          },
          value: this.helpers.isNumber(val) ? parseFloat(val) : val,
          formatted_value: val,
          style: {}
        };
        if (links && links.length) {
          cellData.link = {
            external: true,
            address: links[0].href
          };
        }

        // cell style
        cellData.style = this.cssToStyles(window.getComputedStyle(cells[j]).cssText); // @todo: We should do this only for new cells - very expensive
        if (cells[j].dataset.format) {
          cellData.style['number-format'] = cells[j].dataset.format;
        }
        if (!rowHeight) {
          rowHeight = cellData.style.height;
        } else {
          cellData.style.height = rowHeight;
        }
        if (colWidths && colWidths[j]) {
          cellData.style.width = `${colWidths[cellCount]}px`;
        } else {
          if (cellData.style.width === '0px') {
            cellData.style.width = '80px';
          }
        }
        if (cells[j].getElementsByTagName('br').length) {
          cellData.style['text-wrap'] = 'wrap';
        }

        // check if color is valid @todo: bit hacky
        let color: string = cellData.style.color;
        if (color && color.indexOf('#') === -1 && color.indexOf('rgb') === -1) {
          color = `#${color}`;
        }
        if (!this.helpers.validHex(color)) {
          cellData.style.color = '000000';
        }

        // add cell to row
        data[rowCount][cellCount] = cellData;

        // merged rows. ow!
        if (rowSpan > 1) {
          for (let s: number = 1; s < rowSpan; s++) {
            if (!rowSpans[rowCount + s]) rowSpans[rowCount + s] = [];
            let copyCellData: any = JSON.parse(JSON.stringify(cellData));
            copyCellData.value = '';
            copyCellData.formatted_value = '';
            copyCellData.index.row = rowCount + s;
            rowSpans[rowCount + s][cellCount] = copyCellData;
          }
        }

        cellCount++;

        // merged cells TODO: what about vertically merged cells?
        if (colSpan > 1) {
          for (let k: number = 1; k < colSpan; k++) {
            let copyCellData: any = JSON.parse(JSON.stringify(cellData));
            copyCellData.value = '';
            copyCellData.formatted_value = '';
            // copyCellData.index = merge({}, cellData.index);
            if (colWidths && colWidths[cellCount]) {
              copyCellData.style.width = `${colWidths[cellCount]}px`;
            }
            copyCellData.index.col = cellCount;
            data[rowCount][cellCount] = copyCellData;
            if (rowSpan > 1) {
              for (let s: number = 1; s < rowSpan; s++) {
                copyCellData = rowSpans[rowCount + s][cellCount] || JSON.parse(JSON.stringify(copyCellData));
                copyCellData.index.row = rowCount + s;
                rowSpans[rowCount + s][cellCount] = copyCellData;
              }
            }
            cellCount++;
          }
        }
      }

      rowCount++;
    }

    for (let i: number = 0; i < data.length; i++) {
      if (data[i].length >= maxColumns) {
        continue;
      }
      let cell: any = data[i][data[i].length - 1];
      cell.value = '';
      cell.formatted_value = '';
      for (let k: number = data[i].length; k < maxColumns; k++) {
        data[i].push(cell);
      }
    }

    console.log(data);

    return data;
  }

  private tableColWidths(table: any): any {
    let colGroup: any = table.getElementsByTagName('colgroup');
    if (!colGroup.length) {
      return undefined;
    }
    let colWidths: any = [];
    let cols: any = colGroup[0].getElementsByTagName('col');
    for (let i: number = 0; i < cols.length; i++) {
      colWidths.push(parseInt(cols[i].width, 10));
      if (cols[i].span && cols[i].span > 1) {
        for (let k: number = 0; k < cols[i].span - 1; k++) {
          colWidths.push(parseInt(cols[i].width, 10));
        }
      }
    }
    return colWidths;
  }

  private getRawValue(val: any, format: any): string {
    if (typeof format === 'undefined') {
      format = '@';
    }
    if (format === '@') {
      return val;
    } // Plain text
    return val;
  }

  private cssToStyles(cssText: any): void {
    let parts: any = cssText.split(';'),
      style: any = {};

    for (let i: number = 0; i < parts.length; i++) {
      let styleParts: any = parts[i].split(':');

      if (styleParts.length < 2) {
        continue;
      }

      let styleName: any = styleParts[0].trim();
      let styleVal: any = styleParts[1].trim();

      // Ignore styles we dont need
      let hasBorder: boolean = false;
      if (styleName.indexOf('border-') > -1) {
        let nameParts: string[] = styleName.split('-');
        if (
          nameParts.length === 3 &&
          this.borderSides.indexOf(nameParts[1]) !== -1 &&
          this.borderSyles.indexOf(nameParts[2]) !== -1
        ) {
          hasBorder = true;
        }
      }
      if (this.validStyles.indexOf(styleName) === -1 && !hasBorder) {
        continue;
      }

      // Strip !important, #
      styleVal = styleVal.split('!')[0];

      // Font family
      if (styleName === 'font-family') {
        styleVal = styleVal.split(',')[0].replace(/"/gi, '');
      }

      // Colors
      if (
        (styleName === 'color' || styleName === 'background-color' || styleName.indexOf('-color') !== -1) &&
        styleVal.indexOf('rgb') >= 0
      ) {
        styleVal = `${this.helpers.rgbToHex(styleVal)}`;
      }

      // Font size
      if (styleName === 'font-size' && styleVal.indexOf('px')) {
        styleVal = `${Math.round(parseFloat(styleVal) * 0.73)}pt`;
      }

      // text wrap (for excel)
      if (styleName === 'word-wrap') {
        style['text-wrap'] = styleVal.trim();
      }
      // workaround for ng-ipushpull
      if (styleName === 'white-space' && styleVal === 'normal') {
        styleVal = 'inherit';
      }
      if (styleName === 'text-align') {
        if (styleVal.indexOf('right') > -1) {
          styleVal = 'right';
        } else if (styleVal.indexOf('justify') > -1 || styleVal.indexOf('left') > -1) {
          styleVal = 'left';
        } else if (styleVal.indexOf('center') > -1) {
          styleVal = 'center';
        }
      }

      style[styleName] = styleVal.trim();

      if (hasBorder) {
        let map: any = this.mapExcelBorder(styleName, styleVal);
        if (map) {
          style[map.name] = map.value;
        }
      }
    }

    return style;
  }

  private mapExcelBorder(key: string, val: string): any {
    let parts: string[] = key.split('-');
    if (parts.length !== 3) {
      return undefined;
    }
    let name: any = [parts[1].substr(0, 1), parts[0].substr(0, 1)];
    if (this.excelStyles.indexOf(name.join('')) === -1 || this.borderSyles.indexOf(parts[2]) === -1) {
      return undefined;
    }
    let index: number = this.borderSyles.indexOf(parts[2]);
    let style: string = this.borderSyles[index];
    name.push(style.substr(0, 1));

    let value: string = '';
    switch (style) {
      case 'width':
        value = this.excelBorderWeights[`${Math.round(parseFloat(val))}px`] || 'none';
        break;
      case 'style':
        value = this.excelBorderStyles[val] || 'none';
        break;
      default:
        value = val;
        break;
    }

    let map: any = {
      name: name.join(''),
      value: value
    };
    return map;
  }

  private getTextWidth(text: any, font: string): number {
    // re-use canvas object for better performance
    let canvas: any = this.canvas || (this.canvas = document.createElement('canvas'));
    let context: any = canvas.getContext('2d');
    context.font = font;
    let metrics: any = context.measureText(text);
    return metrics.width;
  }
}
