
const OperationType = {
  LOGIC: 'LOGIC',
  STRING: 'STRING',
  COMPARISON: 'COMPARISON',
  MATH: 'MATH',
};

export class Lexer {
  // operation table
  static get optable() {
    return {
      '=': OperationType.LOGIC,
      '&': OperationType.LOGIC,
      '|': OperationType.LOGIC,
      '?': OperationType.LOGIC,
      ':': OperationType.LOGIC,

      '\'': OperationType.STRING,
      '"': OperationType.STRING,

      '!': OperationType.COMPARISON,
      '>': OperationType.COMPARISON,
      '<': OperationType.COMPARISON,

      '(': OperationType.MATH,
      ')': OperationType.MATH,
      '+': OperationType.MATH,
      '-': OperationType.MATH,
      '*': OperationType.MATH,
      '/': OperationType.MATH,
      '%': OperationType.MATH,
    };
  }

  /**
   * Constructor
   * @param {*} expression -
   */
  constructor(expression) {
    this.currentIndex = 0;
    this.input = expression;
    this.tokenList = [];
  }

  getTokens() {
    let tok;
    do {
      // read current token, so step should be -1
      tok = this.pickNext(-1);
      const pos = this.currentIndex;
      switch (Lexer.optable[tok]) {
        case OperationType.LOGIC:
          // == && || ===
          this.readLogicOpt(tok);
          break;

        case OperationType.STRING:
          this.readString(tok);
          break;

        case OperationType.COMPARISON:
          this.readCompare(tok);
          break;

        case OperationType.MATH:
          this.receiveToken();
          break;

        default:
          this.readValue(tok);
      }

      // if the pos not changed, this loop will go into a infinite loop, every step of while loop,
      // we must move the pos forward
      // so here we should throw error, for example `1 & 2`
      if (pos === this.currentIndex && tok !== undefined) {
        const err = new Error(`unkonw token ${tok} from input string ${this.input}`);
        err.name = 'UnknowToken';
        throw err;
      }
    } while (tok !== undefined)

    return this.tokenList;
  }

  /**
   * read next token, the index param can set next step, default go foward 1 step
   *
   * @param index next postion
   */
  pickNext(index = 0) {
    return this.input[index + this.currentIndex + 1];
  }

  /**
   * Store token into result tokenList, and move the pos index
   *
   * @param index
   */
  receiveToken(index = 1) {
    const tok = this.input.slice(this.currentIndex, this.currentIndex + index).trim();
    // skip empty string
    if (tok) {
      this.tokenList.push(tok);
    }

    this.currentIndex += index;
  }

  // ' or "
  readString(tok) {
    let next;
    let index = 0;
    do {
      next = this.pickNext(index);
      index += 1;
    } while (next !== tok && next !== undefined);
    this.receiveToken(index + 1);
  }

  // > or < or >= or <= or !==
  // tok in (>, <, !)
  readCompare(tok) {
    if (this.pickNext() !== '=') {
      this.receiveToken(1);
      return;
    }
    // !==
    if (tok === '!' && this.pickNext(1) === '=') {
      this.receiveToken(3);
      return;
    }
    this.receiveToken(2);
  }

  // === or ==
  // && ||
  readLogicOpt(tok) {
    if (this.pickNext() === tok) {
      // ===
      if (tok === '=' && this.pickNext(1) === tok) {
        return this.receiveToken(3);
      }
      // == && ||
      return this.receiveToken(2);
    }
    // handle as &&
    // a ? b : c is equal to a && b || c
    if (tok === '?' || tok === ':') {
      return this.receiveToken(1);
    }
  }

  readValue(tok) {
    if (!tok) {
      return;
    }

    let index = 0;
    while (!Lexer.optable[tok] && tok !== undefined) {
      tok = this.pickNext(index);
      index += 1;
    }
    this.receiveToken(index);
  }
}

export default function token(expression) {
  const lexer = new Lexer(expression);
  return lexer.getTokens();
}
