import EventEmitter from 'eventemitter3';
import sleep from 'sleep-promise';
import Terminal from 'terminal.js';

import Socket from '../../socket';
import {
  decode,
  encode,
  keymap as key,
} from '../../utils';
import {
  getWidth,
  indexOfWidth,
  substrWidth,
} from '../../utils/char';
import Config from '../../config';

import defaultConfig from './config';
import {Article, Board} from './model';

class Condition {
  private typeWord: string;
  private criteria: string;

  constructor(type: 'push'|'author'|'title', criteria: string) {
    switch (type) {
      case 'push':
        this.typeWord = 'Z';
        break;
      case 'author':
        this.typeWord = 'a';
        break;
      case 'title':
        this.typeWord = '/';
        break;
      default:
        throw new Error(`Invalid condition: ${type}`);
    }
    this.criteria = criteria;
  }

  toSearchString(): string {
    return `${this.typeWord}${this.criteria}`;
  }
}

class Bot extends EventEmitter {
  static initialState = {
    connect: false,
    login: false,
  };
  static forwardEvents = [
    'message',
    'error',
  ];

  searchCondition = {
    conditions: null,
    init: function() {
      this.conditions = [];
    },
    add: function(type, criteria) {
      this.conditions.push(new Condition(type, criteria));
    }
  };

  private config: Config;
  private term: Terminal;
  private _state: any;
  private currentCharset: string;
  private socket: Socket;
  private preventIdleHandler: ReturnType<typeof setTimeout>;

  constructor(config?: Config) {
    super();
    this.config = {...defaultConfig, ...config};
    this.init();
  }

  async init(): Promise<void> {
    const { config } = this;
    this.term = new Terminal(config.terminal);
    this._state = { ...Bot.initialState };
    this.term.state.setMode('stringWidth', 'dbcs');
    this.currentCharset = 'big5';

    switch (config.protocol.toLowerCase()) {
      case 'websocket':
      case 'ws':
      case 'wss':
        break;
      case 'telnet':
      case 'ssh':
      default:
        throw new Error(`Invalid protocol: ${config.protocol}`);
        break;
    }

    const socket = new Socket(config);
    socket.connect();

    Bot.forwardEvents.forEach(e => {
      socket.on(e, this.emit.bind(this, e));
    });
    socket
      .on('connect', (...args) => {
        this._state.connect = true;
        this.emit('connect', ...args);
        this.emit('stateChange', this.state);
      })
      .on('disconnect', (closeEvent, ...args) => {
        this._state.connect = false;
        this.emit('disconnect', closeEvent, ...args);
        this.emit('stateChange', this.state);
      })
      .on('message', (data) => {
        if (this.currentCharset !== this.config.charset && !this.state.login &&
            decode(data, 'utf8').includes('登入中，請稍候...')) {
          this.currentCharset = this.config.charset;
        }
        const msg = decode(data, this.currentCharset);
        this.term.write(msg);
        this.emit('redraw', this.term.toString());
      })
      .on('error', (err) => {
      });
    this.socket = socket;
  }

  get state(): any {
    return {...this._state};
  }

  getLine = (n) => {
    return this.term.state.getLine(n);
  }

  async getLines() {
    const { getLine } = this;
    const lines = [];

    lines.push(getLine(0).str);

    let sentPgDown = false;
    while (!getLine(23).str.includes('100%')
        && !getLine(23).str.includes('此文章無內容')) {
      for (let i = 1; i < 23; i++) {
        lines.push(getLine(i).str);
      }
      await this.send(key.PgDown);
      sentPgDown = true;
    }

    const lastLine = lines[lines.length - 1];
    for (let i = 0; i < 23; i++) {
      if (getLine(i).str === lastLine) {
        for (let j = i + 1; j < 23; j++) {
          lines.push(getLine(j).str);
        }
        break;
      }
    }

    while (lines.length > 0 && lines[lines.length - 1] === '') {
      lines.pop();
    }

    if (sentPgDown) {
      await this.send(key.Home);
    }
    return lines;
  }

  send(msg: string): Promise<boolean> {
    if (this.config.preventIdleTimeout) {
        this.preventIdle(this.config.preventIdleTimeout);
    }
    return new Promise((resolve, reject) => {
      let autoResolveHandler;
      const cb = message => {
        clearTimeout(autoResolveHandler);
        resolve(true);
      };
      if (this.state.connect) {
        if (msg.length > 0) {
          this.socket.send(encode(msg, this.currentCharset));
          this.once('message', cb);
          autoResolveHandler = setTimeout(() => {
            this.removeListener('message', cb);
            resolve(false);
          }, this.config.timeout * 10);
        } else {
          console.warn(`Sending message with 0-length`);
          resolve(true);
        }
      } else {
        reject();
      }
    });
  }

  preventIdle(timeout: number): void {
    clearTimeout(this.preventIdleHandler);
    if (this.state.login) {
      this.preventIdleHandler = setTimeout(async () => {
        await this.send(key.CtrlU);
        await this.send(key.ArrowLeft);
      }, timeout * 1000);
    }
  }

  async login(username: string, password: string, kick: boolean= true): Promise<any> {
    if (this.state.login) { return; }
    username = username.replace(/,/g, '');
    if (this.config.charset === 'utf8') {
      username += ',';
    }
    await this.send(`${username}${key.Enter}${password}${key.Enter}`);
    let ret;
    while ((ret = await this.checkLogin(kick)) === null) {
      await sleep(400);
    }
    if (ret) {
      const { _state: state } = this;
      state.login = true;
      state.position = {
        boardname: '',
      };
      this.searchCondition.init();
      this.emit('stateChange', this.state);
    }
    return ret;
  }

  async logout(): Promise<boolean> {
    if (!this.state.login) { return; }
    await this.send(`G${key.Enter}Y${key.Enter}`);
    this._state.login = false;
    this.emit('stateChange', this.state);
    this.send(key.Enter);
    return true;
  }

  private async checkLogin(kick: boolean): Promise<any> {
    const { getLine } = this;

    if (getLine(21).str.includes('密碼不對或無此帳號')) {
      this.emit('login.failed');
      return false;
    } else if (getLine(23).str.includes('請稍後再試')) {
      this.emit('login.failed');
      return false;
    } else if (getLine(22).str.includes('您想刪除其他重複登入的連線嗎')) {
      await this.send(`${key.Backspace}${kick ? 'y' : 'n'}${key.Enter}`);
    } else if (getLine(23).str.includes('請勿頻繁登入以免造成系統過度負荷')) {
      await this.send(`${key.Enter}`);
    } else if (getLine(23).str.includes('按任意鍵繼續')) {
      await this.send(`${key.Enter}`);
    } else if (getLine(23).str.includes('您要刪除以上錯誤嘗試的記錄嗎')) {
      await this.send(`${key.Backspace}y${key.Enter}`);
    } else if ((getLine(22).str + getLine(23).str).toLowerCase().includes('y/n')) {
      await this.send(`${key.Backspace}y${key.Enter}`);
    } else if (getLine(23).str.includes('我是')) {
      this.emit('login.success');
      return true;
    } else {
      await this.send(`q`);
    }
    return null;
  }

  private checkArticleWithHeader(): boolean {
    const authorArea = substrWidth('dbcs', this.getLine(0).str, 0, 6).trim();
    return authorArea === '作者';
  }

  select(model) {
    return model.select(this);
  }

  /**
   * @deprecated
   */
  setSearchCondition(type: string, criteria: string): void {
    this.searchCondition.add(type, criteria);
  }

  /**
   * @deprecated
   */
  resetSearchCondition(): void {
    this.searchCondition.init();
  }

  /**
   * @deprecated
   */
  isSearchConditionSet(): boolean {
    return (this.searchCondition.conditions.length !== 0);
  }

  /**
   * @deprecated
   */
  async getArticles(boardname: string, offset: number= 0): Promise<Article[]> {
    await this.enterBoard(boardname);
    if (this.isSearchConditionSet()) {
      const searchString = this.searchCondition.conditions.map(condition => condition.toSearchString()).join(key.Enter);
      await this.send(`${searchString}${key.Enter}`);
    }

    if (offset > 0) {
      offset = Math.max(offset - 9, 1);
      await this.send(`${key.End}${key.End}${offset}${key.Enter}`);
    }
    const { getLine } = this;
    const articles: Article[] = [];
    for (let i = 3; i <= 22; i++) {
      const line = getLine(i).str;
      const article = Article.fromLine(line);
      article.boardname = boardname;
      articles.push(article);
    }
    // fix id
    if (articles.length >= 2 && articles[0].id === 0) {
      for (let i = 1; i < articles.length; i++) {
        if (articles[i].id !== 0) {
          articles[0].id = articles[i].id - i;
          break;
        }
      }
    }
    for (let i = 1; i < articles.length; i++) {
      articles[i].id = articles[i - 1].id + 1;
    }
    await this.enterIndex();
    return articles.reverse();
  }

  /**
   * @deprecated
   */
  async getArticle(boardname: string, id: number, article: Article = new Article()): Promise<Article> {
    await this.enterBoard(boardname);
    if (this.isSearchConditionSet()) {
      const searchString = this.searchCondition.conditions.map(condition => condition.toSearchString()).join(key.Enter);
      await this.send(`${searchString}${key.Enter}`);
    }
    const { getLine } = this;

    await this.send(`${id}${key.Enter}${key.Enter}`);

    const hasHeader = this.checkArticleWithHeader();

    article.id = id;
    article.boardname = boardname;

    if (hasHeader) {
      article.author    = substrWidth('dbcs', getLine(0).str, 7, 50).trim();
      article.title     = substrWidth('dbcs', getLine(1).str, 7    ).trim();
      article.timestamp = substrWidth('dbcs', getLine(2).str, 7    ).trim();
    }

    article.lines = await this.getLines();

    await this.enterIndex();
    return article;
  }

  /**
   * @deprecated
   */
  async getFavorite(offsets: number|number[]= []) {
    if (typeof offsets === 'number') {
      offsets = [offsets];
    }
    await this.enterFavorite(offsets);
    const { getLine } = this;

    const favorites: Board[] = [];

    while (true) {
      let stopLoop = false;
      for (let i = 3; i < 23; i++) {
        const line = getLine(i).str;
        if (line.trim() === '') {
          stopLoop = true;
          break;
        }
        const favorite = Board.fromLine(line);
        if (favorite.id !== favorites.length + 1) {
          stopLoop = true;
          break;
        }
        favorites.push(favorite);
      }
      if (stopLoop) {
        break;
      }
      await this.send(key.PgDown);
    }

    await this.enterIndex();
    return favorites;
  }

  async getMails(offset: number= 0) {
    await this.enterMail();
    if (offset > 0) {
      offset = Math.max(offset - 9, 1);
      await this.send(`${key.End}${key.End}${offset}${key.Enter}`);
    }

    const { getLine } = this;

    const mails = [];
    for (let i = 3; i <= 22; i++) {
      const line = getLine(i).str;
      const mail = {
        sn:    +substrWidth('dbcs', line, 1,   5).trim(),
        date:   substrWidth('dbcs', line, 9,   5).trim(),
        author: substrWidth('dbcs', line, 15, 12).trim(),
        status: substrWidth('dbcs', line, 30,  2).trim(),
        title:  substrWidth('dbcs', line, 33    ).trim(),
      };
      mails.push(mail);
    }

    await this.enterIndex();
    return mails.reverse();
  }

  async getMail(sn: number) {
    await this.enterMail();
    const { getLine } = this;

    await this.send(`${sn}${key.Enter}${key.Enter}`);

    const hasHeader = this.checkArticleWithHeader();

    const mail = {
      sn,
      author: '',
      title: '',
      timestamp: '',
      lines: [],
    };

    if (this.checkArticleWithHeader()) {
      mail.author    = substrWidth('dbcs', getLine(0).str, 7, 50).trim();
      mail.title     = substrWidth('dbcs', getLine(1).str, 7    ).trim();
      mail.timestamp = substrWidth('dbcs', getLine(2).str, 7    ).trim();
    }

    mail.lines = await this.getLines();

    await this.enterIndex();
    return mail;
  }

  async enterIndex(): Promise<boolean> {
    await this.send(`${key.ArrowLeft.repeat(10)}`);
    return true;
  }

  get currentBoardname(): string|undefined {
    const boardRe = /【(?!看板列表).*】.*《(?<boardname>.*)》/;
    const match = boardRe.exec(this.getLine(0).str);
    if (match) {
      return match.groups.boardname;
    } else {
      return void 0;
    }
  }

  /**
   * @deprecated
   */
  enterBoard(boardname: string): Promise<boolean> {
    return this.enterBoardByName(boardname);
  }

  async enterBoardByName(boardname: string): Promise<boolean> {
    await this.send(`s${boardname}${key.Enter} ${key.Home}${key.End}`);

    if (this.currentBoardname.toLowerCase() === boardname.toLowerCase()) {
      this._state.position.boardname = this.currentBoardname;
      this.emit('stateChange', this.state);
      return true;
    } else {
      await this.enterIndex();
      return false;
    }
  }

  async enterByOffset(offsets: number[]= []): Promise<boolean> {
    const { getLine } = this;
    let result = true;
    offsets.forEach(async offset => {
      if (offset === 0) {
        result = false;
      }
      if (offset < 0) {
        for (let i = 22; i >= 3; i--) {
          let lastOffset = substrWidth('dbcs', getLine(i).str, 3, 4).trim();
          if (lastOffset.length > 0) {
            offset += +lastOffset + 1;
            break;
          }
          lastOffset = substrWidth('dbcs', getLine(i).str, 15, 2).trim();
          if (lastOffset.length > 0) {
            offset += +lastOffset + 1;
            break;
          }
        }
      }
      if (offset < 0) {
        result = false;
      }
      if (!result) {
        return;
      }
      await this.send(`${offset}${key.Enter.repeat(2)} ${key.Home}${key.End}`);
    });

    if (result) {
      this._state.position.boardname = this.currentBoardname;
      this.emit('stateChange', this.state);
      await this.send(key.Home);
      return true;
    } else {
      await this.enterIndex();
      return false;
    }
  }

  async enterBoardByOffset(offsets: number[]= []): Promise<boolean> {
    await this.send(`C${key.Enter}`);
    return await this.enterByOffset(offsets);
  }

  async enterFavorite(offsets: number[]= []): Promise<boolean> {
    await this.send(`F${key.Enter}`);
    return await this.enterByOffset(offsets);
  }

  async enterMail(): Promise<boolean> {
    await this.send(`M${key.Enter}R${key.Enter}${key.Home}${key.End}`);
    return true;
  }
}

export default Bot;
