'use strict';

const Service = require('egg').Service;
const Excel = require('exceljs');
const Stream = require('stream');
const _ = require('lodash');
const contentDisposition = require('content-disposition');

const DATA = Symbol('excel_data');
const CALL = Symbol('excel_call');

class ExcelHelper {
  async get(arg) {
    return this._get(arg);
  }

  getObjectValue(obj, key) {
    const keys = key.split('.');
    let ret = obj;
    for (let i = 0; i < keys.length; i++) {
      if (ret !== undefined) {
        ret = ret[keys[i]];
      } else {
        return undefined;
      }
    }
    return ret;
  }
}

class ExcelExportHelper extends ExcelHelper {
  constructor(ctx) {
    super();
    this.ctx = ctx;
    this.sheets = [];
    this.fileName = '未命名' + (+new Date());
  }

  setFileName(fileName) {
    this.fileName = fileName;
    return this;
  }

  newSheet(data, sheetName = false) {
    if (!sheetName) {
      sheetName = 'sheet' + (this.sheets.length + 1);
    }
    this.sheets.push({
      data,
      sheetName,
      columns: [],
    });
    return this;
  }

  newColumn(key, option) {
    const lastSheet = this.sheets[this.sheets.length - 1];
    const param = {};
    if (lastSheet) {
      if (typeof option === 'string') {
        param.header = option;
      } else {
        param.header = option.title || key;
        param.format = option.format;
      }
    }
    param.key = key;
    if (/\./.test(key)) {
      if (!param.format) {
        param.format = (val, i, row) => {
          return this.getObjectValue(row, key);
        };
      } else {
        const srcFormat = param.format;
        param.format = (val, i, row) => {
          return srcFormat(this.getObjectValue(row, key), row, i);
        };
      }
    }
    lastSheet.columns.push(param);
    return this;
  }

  getFixedCellWidth(v) {
    return Math.max(10, ('' + v).length * 2);
  }

  async _get() {
    const stream = new Stream.PassThrough();
    // const workbook = new Excel.Workbook();
    const workbook = new Excel.stream.xlsx.WorkbookWriter({
      stream,
      useStyles: true,
      useSharedStrings: true,
    });
    this.sheets.forEach(item => {
      const worksheet = workbook.addWorksheet(item.sheetName);
      const formatList = [];
      if (item.data.length) {
        const row = item.data[0];
        if (item.columns.length === 0) {
          for (const k in row) {
            const v = row[k];
            const type = typeof v;
            if (type === 'string' || type === 'number') {
              item.columns.push({
                header: k,
                key: k,
                width: this.getFixedCellWidth(v),
              });
            }
          }
        } else {
          for (let i = 0; i < item.columns.length; i++) {
            const column = item.columns[i];
            if (!column.width) {
              column.width = this.getFixedCellWidth(row[column.key]);
            }
            if (column.format) {
              formatList.push({
                key: column.key,
                fn: column.format,
              });
            }
          }
        }
      }

      worksheet.columns = item.columns;
      for (let i = 0; i < item.data.length; i++) {
        let row = item.data[i];
        if (row instanceof Object) {
          row = JSON.parse(JSON.stringify(row));
        } else {
          row = {};
        }
        for (let i = 0; i < formatList.length; i++) {
          const item = formatList[i];
          row[item.key] = item.fn(row[item.key], i, row);
        }
        worksheet.addRow(row).commit();
      }
    });
    let { fileName } = this;
    fileName += '.xlsx';
    // fileName = encodeURIComponent(fileName);
    // this.ctx.set('content-disposition', 'attachment;filename=' + fileName);
    this.ctx.set('content-disposition', contentDisposition(fileName));
    workbook.commit();
    this.ctx.body = stream;
    this.ctx.status = 200;
    return stream;
  }
}

class ExcelImportHelper extends ExcelHelper {
  constructor(ctx) {
    super();
    this.ctx = ctx;
    this.columns = [];
    this[DATA] = false;
    this.lineNumberKey = false;
  }

  showLineNumber(key) {
    this.lineNumberKey = key;
    return this;
  }

  newColumn(header, option) {
    const param = {};
    if (typeof option === 'string') {
      param.key = option;
    } else {
      param.key = option.key || header;
      param.format = option.format;
      param.type = option.type;
      param.required = option.required;
      if (option.expect) {
        const { expect } = option;
        if (expect instanceof RegExp) {
          param.expect = (val, rowNumber, colNumber) => {
            if (!expect.test(val)) {
              this.ctx.failed(`第${rowNumber}行第${colNumber}列${header}格式不正确`);
            }
          };
        } else if (expect instanceof Array) {
          param.expect = (val, rowNumber, colNumber) => {
            if (expect.indexOf(val) === -1) {
              this.ctx.failed(`第${rowNumber}行第${colNumber}列${header}只能为如下值：`
                + expect.join('、'));
            }
          };
        }
      }
    }
    param.header = header;
    this.columns.push(param);
    return this;
  }

  [CALL](i) {
    const data = this[DATA][i];
    const columns = [];
    const ret = [];
    if (data.length) {
      const { values } = data[0];
      for (let i = 0; i < this.columns.length; i++) {
        const header = this.columns[i].header;
        let find = false;
        for (let j = 1; j < values.length; j++) {
          let val = values[j];
          if (val && val.richText) {
            val = val.richText.map(item => item.text).join('');
          }
          if (val === header) {
            const {
              key,
              format,
              type,
              expect,
              required,
            } = this.columns[i];
            columns.push({
              index: j,
              key,
              format,
              type,
              expect,
              required,
            });
            find = true;
            break;
          }
        }
        if (!find) {
          this.ctx.failed(`缺失列[${header}]`);
        }
      }
    }
    for (let i = 1; i < data.length; i++) {
      const dict = {};
      const { values, rowNumber } = data[i];
      if (this.lineNumberKey) {
        dict[this.lineNumberKey] = rowNumber;
      }
      for (let j = 0; j < columns.length; j++) {
        const c = columns[j];
        const colNumber = c.index;
        let v = values[colNumber];
        if (v instanceof Object) {
          if ('text' in v) {
            v = v.text;
          } else if ('richText' in v) {
            v = v.richText.map(item => item.text).join('');
          } else if ('result' in v) {
            v = v.result;
            if (_.isNumber(v)) {
              const tmp = Number(v.toFixed(4));
              if (Math.abs(v - tmp) < 0.00000000001) {
                v = tmp;
              }
            }
          }
        }
        if (c.trim && _.isString(v)) {
          v = _.trim(v);
        }
        if (v === undefined || v === '') {
          if (c.required) {
            this.ctx.failed(`第${rowNumber}行第${colNumber}列不能为空`);
          }
        }
        switch (c.type) {
          case 'int':
          case 'integer':
            if (!_.isInteger(v)) {
              if (_.isString(v) && /^\s*\d+\s*$/.test(v)) {
                v = +v;
              } else if (v === undefined || v === '') {
                v = null;
              } else {
                this.ctx.failed(`第${rowNumber}行第${colNumber}列不是整数`);
              }
            }
            break;
          case 'number':
            if (!_.isNumber(v)) {
              if (_.isString(v) && /^\s*\d+(?:\.\d+)?\s*$/.test(v)) {
                v = +v;
              } else if (v === undefined || v === '') {
                v = null;
              } else {
                this.ctx.failed(`第${rowNumber}行第${colNumber}列不是数字`);
              }
            }
            break;
          case 'string':
            if (!_.isString(v)) {
              if (_.isNumber(v)) {
                v = '' + v;
              } else {
                this.ctx.failed(`第${rowNumber}行第${colNumber}列不是字符串`);
              }
            }
            break;
          default:
            break;
        }
        c.expect && c.expect(v, rowNumber, colNumber);
        dict[c.key] = c.format ? c.format(v, rowNumber, colNumber) : v;
      }
      ret.push(dict);
    }
    this.lineNumberKey = false;
    return ret;
  }

  async _get(stream) {
    const sheetIndex = 0;
    if (this[DATA]) {
      return this[CALL](sheetIndex);
    }
    stream = stream || await this.ctx.getFileStream();
    const workbook = new Excel.Workbook();
    const data = [];
    await workbook.xlsx.read(stream).then(workbook => {
      workbook.eachSheet(function(worksheet) {
        const sheetData = [];
        worksheet.eachRow(function(row, rowNumber) {
          sheetData.push({ rowNumber, values: row.values });
        });
        data.push(sheetData);
      });
    });
    this[DATA] = data;
    return this[CALL](sheetIndex);
  }
}

class ExcelService extends Service {
  newExport() {
    return new ExcelExportHelper(this.ctx);
  }

  newImport() {
    return new ExcelImportHelper(this.ctx);
  }
}

module.exports = ExcelService;
