import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

import { RPCClientError } from '../protocol/RPCError';
import { RPCClientRWC } from './RPCClientRWC';

import {
  RPCClientCodec,
  RPCClientResponseCodec,
  RPCClientNotificationCodec,
} from '../protocol/RPCClientCodec';

export interface RPCRequestState {
  subject: Subject<any>;
  request: {
    method: string;
    params: any[];
  };
}

export abstract class RPCClient {
  private _requestID: number;

  private _pendingRequestsCount: number;
  private _pendingRequests: Map<number, RPCRequestState>;

  public constructor(
    private _codec: RPCClientCodec,
    private _rwc: RPCClientRWC,
  ) {
    this._requestID = 0;
    this._pendingRequestsCount = 0;
    this._pendingRequests = new Map();
  }

  private getRequestID(): number {
    return ++this._requestID;
  }

  /**
   * connect
   */
  public connect(queryString?: string): void {
    this._rwc.connect(queryString);
    this._rwc.readResponse().subscribe(
      (value: Object) => {
        this.onMessage(value);
      },
      (error: any) => {
        console.error(error);
      },
      () => {

      }
    );
  }

  /**
   * close
   */
  public disconnect() {
    this._rwc.disconnect();
  }

  /**
   * notify
   */
  public send(method: string, ...args: any[]): void {
    this.sendInternal(false, method, args);
  }

  /**
   * call
   */
  public call<T>(method: string, ...args: any[]): Observable<T> {
    return this.sendInternal<T>(true, method, args);
  }

  /**
   * callTimeout
   */
  public callTimeout<T>(ms: number, method: string, ...args: any[]): Observable<T> {

    return undefined;
  }

  private sendInternal<T>(hasResponse: boolean, method: string, args?: any[]): Observable<T> | undefined {
    let id: number;
    let resSubject: Subject<T>;
    if (hasResponse) {
      id = this.getRequestID();
      resSubject = new Subject<T>();
      const reqState: RPCRequestState = {
        subject: resSubject,
        request: {
          method: method,
          params: args,
        }
      };
      this._pendingRequests.set(id, reqState);
      this._pendingRequestsCount++;
    }

    const req = this._codec.request(method, args, id);
    this._rwc.writeRequest(req);

    if (undefined !== resSubject) {
      return resSubject.asObservable();
    }
  }

  private onMessage(message: Object): void {
    const resCodec = this._codec.response(message);

    if (resCodec.isNotification()) {
      this.onNotification(resCodec.notification());
    } else {
      this.onResponse(resCodec);
    }
  }

  protected onResponse(resCodec: RPCClientResponseCodec): void {
    const id = resCodec.id();
    const result = resCodec.result();
    const error = resCodec.error();

    const reqState: RPCRequestState = this._pendingRequests.get(id);

    this._pendingRequests.delete(id);
    this._pendingRequestsCount--;

    if (undefined !== result) {
      reqState.subject.next(result);
    } else if (undefined !== error) {
      const rpcClientError: RPCClientError = {
        request: reqState.request,
        response: error,
      };
      console.error(rpcClientError);
      reqState.subject.error(rpcClientError);
    }
  }

  protected abstract onNotification(notiCodec: RPCClientNotificationCodec): void;
}
