const {EventEmitter} = require('events');
const Sax = require('sax');

const _ = require('../../utils/under-dash');
const utils = require('../../utils/utils');
const colCache = require('../../utils/col-cache');
const Dimensions = require('../../doc/range');

const Row = require('../../doc/row');
const Column = require('../../doc/column');

class WorksheetReader extends EventEmitter {
  constructor(workbook, id) {
    super();

    this.workbook = workbook;
    this.id = id;

    // and a name
    this.name = `Sheet${this.id}`;

    // column definitions
    this._columns = null;
    this._keys = {};

    // keep a record of dimensions
    this._dimensions = new Dimensions();
  }

  // destroy - not a valid operation for a streaming writer
  // even though some streamers might be able to, it's a bad idea.
  destroy() {
    throw new Error('Invalid Operation: destroy');
  }

  // return the current dimensions of the writer
  get dimensions() {
    return this._dimensions;
  }

  // =========================================================================
  // Columns

  // get the current columns array.
  get columns() {
    return this._columns;
  }

  // get a single column by col number. If it doesn't exist, it and any gaps before it
  // are created.
  getColumn(c) {
    if (typeof c === 'string') {
      // if it matches a key'd column, return that
      const col = this._keys[c];
      if (col) {
        return col;
      }

      // otherise, assume letter
      c = colCache.l2n(c);
    }
    if (!this._columns) {
      this._columns = [];
    }
    if (c > this._columns.length) {
      let n = this._columns.length + 1;
      while (n <= c) {
        this._columns.push(new Column(this, n++));
      }
    }
    return this._columns[c - 1];
  }

  getColumnKey(key) {
    return this._keys[key];
  }

  setColumnKey(key, value) {
    this._keys[key] = value;
  }

  deleteColumnKey(key) {
    delete this._keys[key];
  }

  eachColumnKey(f) {
    _.each(this._keys, f);
  }

  // =========================================================================
  // Read

  _emitRow(row) {
    this.emit('row', row);
  }

  read(entry, options) {
    let emitSheet = false;
    let emitHyperlinks = false;
    let hyperlinks = null;
    switch (options.worksheets) {
      case 'emit':
        emitSheet = true;
        break;
      case 'prep':
        break;
      default:
        break;
    }
    switch (options.hyperlinks) {
      case 'emit':
        emitHyperlinks = true;
        break;
      case 'cache':
        this.hyperlinks = hyperlinks = {};
        break;
      default:
        break;
    }
    if (!emitSheet && !emitHyperlinks && !hyperlinks) {
      entry.autodrain();
      this.emit('finished');
      return;
    }

    // references
    const {sharedStrings} = this.workbook;
    const {styles} = this.workbook;
    const {properties} = this.workbook;

    // xml position
    let inCols = false;
    let inRows = false;
    let inHyperlinks = false;

    // parse state
    let cols = null;
    let row = null;
    let c = null;
    let current = null;

    const parser = Sax.createStream(true, {});
    parser.on('opentag', node => {
      if (emitSheet) {
        switch (node.name) {
          case 'cols':
            inCols = true;
            cols = [];
            break;
          case 'sheetData':
            inRows = true;
            break;

          case 'col':
            if (inCols) {
              cols.push({
                min: parseInt(node.attributes.min, 10),
                max: parseInt(node.attributes.max, 10),
                width: parseFloat(node.attributes.width),
                styleId: parseInt(node.attributes.style || '0', 10),
              });
            }
            break;

          case 'row':
            if (inRows) {
              const r = parseInt(node.attributes.r, 10);
              row = new Row(this, r);
              if (node.attributes.ht) {
                row.height = parseFloat(node.attributes.ht);
              }
              if (node.attributes.s) {
                const styleId = parseInt(node.attributes.s, 10);
                const style = styles.getStyleModel(styleId);
                if (style) {
                  row.style = style;
                }
              }
            }
            break;
          case 'c':
            if (row) {
              c = {
                ref: node.attributes.r,
                s: parseInt(node.attributes.s, 10),
                t: node.attributes.t,
              };
            }
            break;
          case 'f':
            if (c) {
              current = c.f = {text: ''};
            }
            break;
          case 'v':
            if (c) {
              current = c.v = {text: ''};
            }
            break;
          case 'mergeCell':
            break;
          default:
            break;
        }
      }

      // =================================================================
      //
      if (emitHyperlinks || hyperlinks) {
        switch (node.name) {
          case 'hyperlinks':
            inHyperlinks = true;
            break;
          case 'hyperlink':
            if (inHyperlinks) {
              const hyperlink = {
                ref: node.attributes.ref,
                rId: node.attributes['r:id'],
              };
              if (emitHyperlinks) {
                this.emit('hyperlink', hyperlink);
              } else {
                hyperlinks[hyperlink.ref] = hyperlink;
              }
            }
            break;
          default:
            break;
        }
      }
    });

    // only text data is for sheet values
    parser.on('text', text => {
      if (emitSheet) {
        if (current) {
          current.text += text;
        }
      }
    });

    parser.on('closetag', name => {
      if (emitSheet) {
        switch (name) {
          case 'cols':
            inCols = false;
            this._columns = Column.fromModel(cols);
            break;
          case 'sheetData':
            inRows = false;
            break;

          case 'row':
            this._dimensions.expandRow(row);
            this._emitRow(row);
            row = null;
            break;

          case 'c':
            if (row && c) {
              const address = colCache.decodeAddress(c.ref);
              const cell = row.getCell(address.col);
              if (c.s) {
                const style = styles.getStyleModel(c.s);
                if (style) {
                  cell.style = style;
                }
              }

              if (c.f) {
                const value = {
                  formula: c.f.text,
                };
                if (c.v) {
                  if (c.t === 'str') {
                    value.result = utils.xmlDecode(c.v.text);
                  } else {
                    value.result = parseFloat(c.v.text);
                  }
                }
                cell.value = value;
              } else if (c.v) {
                switch (c.t) {
                  case 's': {
                    const index = parseInt(c.v.text, 10);
                    if (sharedStrings) {
                      cell.value = sharedStrings[index];
                    } else {
                      cell.value = {
                        sharedString: index,
                      };
                    }
                    break;
                  }

                  case 'str':
                    cell.value = utils.xmlDecode(c.v.text);
                    break;

                  case 'e':
                    cell.value = {error: c.v.text};
                    break;

                  case 'b':
                    cell.value = parseInt(c.v.text, 10) !== 0;
                    break;

                  default:
                    if (utils.isDateFmt(cell.numFmt)) {
                      cell.value = utils.excelToDate(parseFloat(c.v.text), properties.model.date1904);
                    } else {
                      cell.value = parseFloat(c.v.text);
                    }
                    break;
                }
              }
              if (hyperlinks) {
                const hyperlink = hyperlinks[c.ref];
                if (hyperlink) {
                  cell.text = cell.value;
                  cell.value = undefined;
                  cell.hyperlink = hyperlink;
                }
              }
              c = null;
            }
            break;
          default:
            break;
        }
      }
      if (emitHyperlinks || hyperlinks) {
        switch (name) {
          case 'hyperlinks':
            inHyperlinks = false;
            break;
          default:
            break;
        }
      }
    });
    parser.on('error', error => {
      this.emit('error', error);
    });
    parser.on('end', () => {
      this.emit('finished');
    });

    // create a down-stream flow-control to regulate the stream
    const flowControl = this.workbook.flowControl.createChild();
    flowControl.pipe(
      parser,
      {sync: true}
    );
    entry.pipe(flowControl);
  }
}

module.exports = WorksheetReader;
