import EventEmitter, { IEmitter } from '../Emitter';
import { IApiService } from '../Api';
import { IAuthService } from '../Auth';
import { IStorageService } from '../Storage';
import { IIPPConfig } from '../Config';
import { IPage, IPageServiceMeta, IPageServiceContent } from './Page';
import Promise from 'bluebird';
import * as io from "socket.io-client";

export interface IPageProvider extends IEmitter {
  start: () => void;
  stop: () => void;
  destroy: () => void;
  getData: () => void;
}

// Main/public page service
let api: IApiService, auth: IAuthService, storage: IStorageService, config: IIPPConfig;

export class Providers {
  public rest!: ProviderREST;
  public socket!: IPageProvider;

  constructor(ippApi: IApiService, ippAuth: IAuthService, ippStorage: IStorageService, ippConf: IIPPConfig) {
    api = ippApi;
    auth = ippAuth;
    storage = ippStorage;
    config = ippConf;
  }
}

// Page rest/polling service
export class ProviderREST extends EventEmitter implements IPageProvider {
  private _stopped: boolean = false;
  private _requestOngoing: boolean = false;
  private _timer: any;
  private _timeout: number = 1000;

  private _seqNo: number = 0;
  private _data: any;
  private _error: boolean = false;

  public set seqNo(seqNo: number) {
    this._seqNo = seqNo;
  }
  public set requestOngoing(value: boolean) {
    this._requestOngoing = value;
  }

  constructor(private _pageId?: number, private _folderId?: number, private _uuid?: string) {
    super();

    this.start();
  }

  /**
   * Start polling for page updates
   */
  public start(): void {
    this._stopped = false;
    this.startPolling();
  }

  /**
   * Stop polling for page updates
   */
  public stop(): void {
    this._stopped = true;
    clearTimeout(this._timer);
  }

  /**
   * Stop polling for page updates and stop all events
   */
  public destroy(): void {
    this.stop();
    this.removeEvent();
  }

  public getData(): any {
    return this._data;
  }

  /**
   * Start the actual polling (loop)
   */
  // @todo Not great
  private startPolling(): void {
    this.load().catch((err) => {
      console.warn(this._requestOngoing ? 'Request in progress' : this._stopped ? 'Polling has stopped' : err);
    });

    this._timer = setTimeout(() => {
      this.startPolling();
    }, this._timeout);
  }

  /**
   * Load page data from service
   * @param ignoreSeqNo
   * @returns {Promise<IPage>}
   */
  private load(ignoreSeqNo: boolean = false): Promise<IPage> {
    let p: Promise<IPage> = new Promise((resolve, reject) => {
      if (this._requestOngoing || this._stopped) {
        // @todo Should we emit something?
        reject(new Error('Error'));
        return;
      }

      this._requestOngoing = true;

      let success: any = (res: any) => {
        // console.log("api.getPage", res);
        if (res.httpCode === 200 || res.httpCode === 204) {
          // New update
          if (res.httpCode === 200) {
            this._seqNo = res.data.seq_no;
            this._timeout = res.data.pull_interval * 1000;
            this._data = res.data;
            this.emit('meta_update', this.tempGetSettingsOb(res.data));
            this.emit('content_update', this.tempGetContentOb(res.data));
          } else {
            // @todo do we need this?
            this.emit('empty_update');
          }
          this._error = false;
          resolve(res.data);
        } else {
          this.onError(res.data);
          reject(new Error('Unable to load page'));
        }
      };

      if (this._uuid) {
        api
          .getPageByUuid({
            uuid: this._uuid,
            seq_no: !ignoreSeqNo ? this._seqNo : undefined
          })
          .then(success, (err) => {
            this.onError(err);
            reject(new Error(err));
          })
          .finally(() => {
            this._requestOngoing = false;
          });
      } else {
        api
          .getPage({
            domainId: this._folderId,
            pageId: this._pageId,
            seq_no: !ignoreSeqNo ? this._seqNo : undefined
          })
          .then(success, (err) => {
            this.onError(err);
            reject(new Error(err));
          })
          .finally(() => {
            this._requestOngoing = false;
          });
      }
    });

    return p;
  }

  private onError(err: any): void {
    this._error = true;
    this.emit('error', err);
  }

  /**
   * Temporary solution to get the required subset of data from full page object
   *
   * @param data
   * @returns {{id: number, seq_no: number, content_modified_timestamp: Date, content: any, content_modified_by: any, push_interval: number, pull_interval: number, is_public: boolean, description: string, encrypted_content: string, encryption_key_used: number, encryption_type_used: number, special_page_type: number}}
   */
  private tempGetContentOb(data: IPage): any {
    return {
      id: data.id,
      name: data.name,
      uuid: data.uuid,
      domain_name: data.domain_name,
      background_color: data.background_color,
      domain_id: data.domain_id,
      seq_no: data.seq_no,
      content_modified_timestamp: data.content_modified_timestamp,
      content: data.content,
      content_modified_by: data.content_modified_by,
      push_interval: data.push_interval,
      pull_interval: data.pull_interval,
      is_public: data.is_public,
      description: data.description,
      encrypted_content: data.encrypted_content,
      encryption_key_used: data.encryption_key_used,
      encryption_type_used: data.encryption_type_used,
      record_history: data.record_history,
      special_page_type: data.special_page_type,
      symphony_sid: data.symphony_sid,
      show_gridlines: data.show_gridlines
    };
  }

  /**
   * Temporary solution to get the required subset of data from full page object
   *
   * @param data
   * @returns {any}
   */
  private tempGetSettingsOb(data: IPage): any {
    return JSON.parse(JSON.stringify(data));
  }
}

// Page sockets service
export class ProviderSocket extends EventEmitter implements IPageProvider {
  public static get SOCKET_EVENT_PAGE_ERROR(): string {
    return 'page_error';
  }

  public static get SOCKET_EVENT_PAGE_CONTENT(): string {
    return 'page_content';
  }

  public static get SOCKET_EVENT_PAGE_PUSH(): string {
    return 'page_push';
  }

  public static get SOCKET_EVENT_PAGE_SETTINGS(): string {
    return 'page_settings';
  }

  public static get SOCKET_EVENT_PAGE_DATA(): string {
    return 'page_data';
  }

  public static get SOCKET_EVENT_PAGE_USER_JOINED(): string {
    return 'page_user_joined';
  }

  public static get SOCKET_EVENT_PAGE_USER_LEFT(): string {
    return 'page_user_left';
  }

  private _stopped: boolean = false;
  private _socket!: SocketIOClient.Socket;
  private _wsUrl: string;
  private _redirectCounter: number = 0;
  private _redirectLimit: number = 10;
  private _data: any;
  private _error: boolean = false;

  constructor(private _pageId?: number, private _folderId?: number) {
    super();

    this._wsUrl = config.ws_url + '/page/' + this._pageId;

    this.start();
  }

  /**
   * Start listening for content updates. If socket not connected yet, it will initialize the socket connection
   */
  public start(): void {
    this._stopped = false;
    if (!this._socket || !this._socket.connected) {
      auth.on(auth.EVENT_LOGIN_REFRESHED, this.onAuthRefresh);

      // Because socket is disconnected on lost access, we need to reconnect it
      this.init();
    } else {
      if (this._data) {
        // this.onPageContent(this._data);
      }
      // this._socket.on(ProviderSocket.SOCKET_EVENT_PAGE_CONTENT, this.onPageContent);
    }
  }

  /**
   * Stop receiving page content updates (still keeps receiving settings/meta updates and other events)
   * This will NOT disconnect the socket, merely it will just stop listening for updates - throws them away
   */
  public stop(): void {
    // this._socket.disconnect();
    // this._socket.off(ProviderSocket.SOCKET_EVENT_PAGE_CONTENT, this.onPageContent);

    this._stopped = true;
  }

  /**
   * Remove listeners for all socket events, disconnects socket and destroys object
   * @param hard
   */
  public destroy(hard: boolean = true): void {
    this._socket.removeAllListeners();
    this._socket.disconnect();
    this.stop();
    auth.off(auth.EVENT_LOGIN_REFRESHED, this.onAuthRefresh);

    if (hard) {
      this.removeEvent();
    }
  }

  public getData(): any {
    return this._data;
  }

  /**
   * Initialize socket connection with all listeners
   */
  private init(): void {
    // Connect to socket
    this._socket = this.connect();

    // Register listeners
    this._socket.on('connect', this.onConnect);
    this._socket.on('reconnecting', this.onReconnectingError);
    this._socket.on('reconnect_error', this.onReconnectError);
    this._socket.on('redirect', this.onRedirect);
    this._socket.on(ProviderSocket.SOCKET_EVENT_PAGE_CONTENT, this.onPageContent);
    this._socket.on(ProviderSocket.SOCKET_EVENT_PAGE_SETTINGS, this.onPageSettings);
    this._socket.on(ProviderSocket.SOCKET_EVENT_PAGE_ERROR, this.onPageError);
    this._socket.on('oauth_error', this.onOAuthError);
    /*this._socket.on(ProviderSocket.SOCKET_EVENT_PAGE_DATA, this.onPageData);
         this._socket.on(ProviderSocket.SOCKET_EVENT_PAGE_USER_JOINED, this.onPageUserJoined);
         this._socket.on(ProviderSocket.SOCKET_EVENT_PAGE_USER_LEFT, this.onPageUserLeft);*/
    this._socket.on('disconnect', this.onDisconnect);

    this._stopped = false;
  }

  /**
   * Connect socket to a server. If client doesnt support websockets, it tries to fall back to long polling
   * @returns {SocketIOClient.Socket}
   */
  private connect(): SocketIOClient.Socket {
    let token: string = api.tokens && api.tokens.access_token ? api.tokens.access_token : storage ? storage.persistent.get('access_token') : '';
    let query: string[] = [`access_token=${token}`];

    query = query.filter((val: string) => {
      return val.length > 0;
    });

    return io.connect(this._wsUrl, {
      query: query.join('&'),
      transports: this.supportsWebSockets() ? ['websocket'] : ['polling'],
      forceNew: true,
      reconnectionAttempts: 5
    });
  }

  /**
   * onConnect event action
   */
  private onConnect: any = () => {
    return;
  };

  private onReconnectingError: any = (attempt: number) => {
    this.emit('error', { message: 'Streaming is currently not available', code: 502, type: 'redirect' });
    this.destroy(true);
    return;
  };

  /**
   * onReconnectError event action
   * Try and brake the previous redirect. Go back to root
   */
  private onReconnectError: any = () => {
    // Destroy current socket
    this.destroy(false);

    this._wsUrl = config.ws_url + '/page/' + this._pageId;

    // Init new socket
    this.start();
  };

  /**
   * onDisconnect event action
   */
  private onDisconnect: any = () => {
    return;
  };

  /**
   * onRedirect event action
   * @param msg
   */
  private onRedirect: any = (msg: string) => {
    // Destroy current socket

    this._wsUrl = msg;

    this._redirectCounter++;
    if (this._redirectCounter >= this._redirectLimit) {
      console.log('socket', this._redirectCounter);
      this.emit('error', { message: 'Streaming connection limit reached', code: 500, type: 'redirect' });
      this.destroy(true);
      this._redirectCounter = 0;
      return;
    } else {
      this.destroy(false);
    }

    // Init new socket

    this.start();
  };

  /**
   * onPageContent eent action
   * @param data
   */
  private onPageContent = (data: IPageServiceContent): void => {
    this._data = data;
    if (!this._stopped) {
      this.emit('content_update', data);
    }
  };

  /**
   * onPageSettings event action
   * @param data
   */
  private onPageSettings = (data: IPageServiceMeta): void => {
    this.emit('meta_update', data);
  };

  /**
   * onPageError event action
   * @param data
   */
  private onPageError = (data: any): void => {
    this._error = true;
    if (data.code === 401) {
      // @todo This is wrong
      auth.emit(auth.EVENT_401);
    } else {
      this.emit('error', data);
    }
  };

  /**
   * onOAuthError event action
   * @param data
   */
  private onOAuthError = (data: any): void => {
    // @todo Do something
    // @todo should we watch auth service for re-logged and re-connect?
    // @todo Emit page error ?
  };

  /**
   * onAuthRefresh event action
   */
  private onAuthRefresh = (): void => {
    // let dummy: number = this._pageId; // This is here just to make the callback different, otherwise event emitter prevents duplicates
    this.start();
  };

  /**
   * Determines if client supports websockets
   * @returns {boolean}
   */
  private supportsWebSockets = () => {
    return 'WebSocket' in window || 'MozWebSocket' in window;
  };
}
