import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { Subject } from 'rxjs/Subject';
import {
  WebSocketSubject,
  WebSocketSubjectConfig
} from 'rxjs/observable/dom/WebSocketSubject';

import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/takeWhile';
import 'rxjs/add/observable/interval';

export interface RxWebsocketSubjectConfig {
  url: string;
  protocol?: string | Array<string>;
  reconnectInterval?: 5000;
  reconnectRetry?: 10;
}

export class RxWebsocketSubject<T> extends Subject<T> {
  private _reconnectionObservable: Observable<number>;
  private _wsSubjectConfig: WebSocketSubjectConfig;
  private _socket: WebSocketSubject<any>;
  private _connectionObserver: Observer<boolean>;
  private _connectionStatus: Observable<boolean>;
  private _queryString: string;

  public constructor(private _config: RxWebsocketSubjectConfig) {
    super();

    this._connectionStatus = new Observable<boolean>((observer) => {
      this._connectionObserver = observer;
    }).share().distinctUntilChanged();

    this._wsSubjectConfig = {
      url: _config.url,
      protocol: _config.protocol,
      closeObserver: {
        next: (e: CloseEvent) => {
          this._socket = null;
          this._connectionObserver.next(false);
        }
      },
      openObserver: {
        next: (e: Event) => {
          this._connectionObserver.next(true);
        }
      },
    };

    this._connectionStatus.subscribe((isConnected: boolean) => {
      if (!this._reconnectionObservable && typeof(isConnected) === 'boolean' && !isConnected) {
        this.reconnect();
      }
    });
  }

  public set queryString(query: string) {
    this._queryString = query;
  }

  public get queryString(): string | undefined {
    return this._queryString;
  }

  public get connectionStatus(): Observable<boolean> {
    return this._connectionStatus;
  }

  public connect(): void {
    const wsSubjectConfig = Object.assign({}, this._wsSubjectConfig);
    if (undefined !== this._queryString) {
      wsSubjectConfig.url = wsSubjectConfig.url + '?' + this._queryString;
    }

    this._socket = new WebSocketSubject(wsSubjectConfig);
    this._socket.subscribe(
      (m) => {
        this.next(m);
      },
      (error: Event) => {
        if (!this._socket) {
          this.reconnect();
        }
      }
    );
  }

  public disconnect(): void {
    this._socket.complete();
  }

  private reconnect(): void {
    this._reconnectionObservable = Observable.interval(this._config.reconnectInterval)
      .takeWhile((v, index) => {
        return index < this._config.reconnectRetry && !this._socket;
    });
    this._reconnectionObservable.subscribe(
      () => {
        this.connect();
      },
      null,
      () => {
        this._reconnectionObservable = null;
        if (!this._socket) {
          this.complete();
          this._connectionObserver.complete();
        }
      }
    );
  }

  public write(data: any): void {
    this._socket.next(data);
  }
}
