import Util from '../util';
import * as Collections from '../utils/collections';
import {default as EventsDispatcher} from "../events/dispatcher";
import Logger from '../logger';
import TransportHooks from './transport_hooks';
import Socket from '../socket';
import Runtime from 'runtime';
import Timeline from '../timeline/timeline';
import TransportConnectionOptions from './transport_connection_options';

/** Provides universal API for transport connections.
 *
 * Transport connection is a low-level object that wraps a connection method
 * and exposes a simple evented interface for the connection state and
 * messaging. It does not implement Pusher-specific WebSocket protocol.
 *
 * Additionally, it fetches resources needed for transport to work and exposes
 * an interface for querying transport features.
 *
 * States:
 * - new - initial state after constructing the object
 * - initializing - during initialization phase, usually fetching resources
 * - intialized - ready to establish a connection
 * - connection - when connection is being established
 * - open - when connection ready to be used
 * - closed - after connection was closed be either side
 *
 * Emits:
 * - error - after the connection raised an error
 *
 * Options:
 * - encrypted - whether connection should use ssl
 * - hostEncrypted - host to connect to when connection is encrypted
 * - hostUnencrypted - host to connect to when connection is not encrypted
 *
 * @param {String} key application key
 * @param {Object} options
 */
export default class TransportConnection extends EventsDispatcher {
  hooks: TransportHooks;
  name: string;
  priority: number;
  key: string;
  options: TransportConnectionOptions;
  state: string;
  timeline: Timeline;
  activityTimeout: number;
  id: number;
  socket: Socket;
  beforeOpen: Function;
  initialize: Function;

  constructor(hooks : TransportHooks, name : string, priority : number, key : string, options : TransportConnectionOptions) {
    super();
    this.initialize = Runtime.transportConnectionInitializer;
    this.hooks = hooks;
    this.name = name;
    this.priority = priority;
    this.key = key;
    this.options = options;

    this.state = "new";
    this.timeline = options.timeline;
    this.activityTimeout = options.activityTimeout;
    this.id = this.timeline.generateUniqueID();
  }

  /** Checks whether the transport handles activity checks by itself.
   *
   * @return {Boolean}
   */
  handlesActivityChecks() : boolean {
    return Boolean(this.hooks.handlesActivityChecks);
  }

  /** Checks whether the transport supports the ping/pong API.
   *
   * @return {Boolean}
   */
  supportsPing() : boolean {
    return Boolean(this.hooks.supportsPing);
  }

  /** Tries to establish a connection.
   *
   * @returns {Boolean} false if transport is in invalid state
   */
  connect() : boolean {
    if (this.socket || this.state !== "initialized") {
      return false;
    }

    var url = this.hooks.urls.getInitial(this.key, this.options);
    try {
      this.socket = this.hooks.getSocket(url, this.options);
    } catch (e) {
      Util.defer(()=> {
        this.onError(e);
        this.changeState("closed");
      });
      return false;
    }

    this.bindListeners();

    Logger.debug("Connecting", { transport: this.name, url});
    this.changeState("connecting");
    return true;
  }

  /** Closes the connection.
   *
   * @return {Boolean} true if there was a connection to close
   */
  close() : boolean {
    if (this.socket) {
      this.socket.close();
      return true;
    } else {
      return false;
    }
  }

  /** Sends data over the open connection.
   *
   * @param {String} data
   * @return {Boolean} true only when in the "open" state
   */
  send(data : any) : boolean {
    if (this.state === "open") {
      // Workaround for MobileSafari bug (see https://gist.github.com/2052006)
      Util.defer(()=> {
        if (this.socket) {
          this.socket.send(data);
        }
      });
      return true;
    } else {
      return false;
    }
  }

  /** Sends a ping if the connection is open and transport supports it. */
  ping() {
    if (this.state === "open" && this.supportsPing()) {
      this.socket.ping();
    }
  }

  private onOpen() {
    if (this.hooks.beforeOpen) {
      this.hooks.beforeOpen(
        this.socket, this.hooks.urls.getPath(this.key, this.options)
      );
    }
    this.changeState("open");
    this.socket.onopen = undefined;
  }

  private onError(error) {
    this.emit("error", { type: 'WebSocketError', error: error });
    this.timeline.error(this.buildTimelineMessage({ error: error.toString() }));
  }

  private onClose(closeEvent?:any) {
    if (closeEvent) {
      this.changeState("closed", {
        code: closeEvent.code,
        reason: closeEvent.reason,
        wasClean: closeEvent.wasClean
      });
    } else {
      this.changeState("closed");
    }
    this.unbindListeners();
    this.socket = undefined;
  }

  private onMessage(message) {
    this.emit("message", message);
  }

  private onActivity() {
    this.emit("activity");
  }

  private bindListeners() {
    this.socket.onopen = ()=> {
      this.onOpen();
    };
    this.socket.onerror = (error) => {
      this.onError(error);
    };
    this.socket.onclose = (closeEvent) => {
      this.onClose(closeEvent);
    };
    this.socket.onmessage = (message) => {
      this.onMessage(message);
    };

    if (this.supportsPing()) {
      this.socket.onactivity = ()=> { this.onActivity(); };
    }
  }

  private unbindListeners() {
    if (this.socket) {
      this.socket.onopen = undefined;
      this.socket.onerror = undefined;
      this.socket.onclose = undefined;
      this.socket.onmessage = undefined;
      if (this.supportsPing()) {
        this.socket.onactivity = undefined;
      }
    }
  }

  private changeState(state : string, params?:any) {
    this.state = state;
    this.timeline.info(this.buildTimelineMessage({
      state: state,
      params: params
    }));
    this.emit(state, params);
  }

  buildTimelineMessage(message) : any {
    return Collections.extend({ cid: this.id }, message);
  }

}
