import { default as EventsDispatcher } from '../events/dispatcher';
import * as Errors from '../errors';
import Logger from '../logger';
import Pusher from '../pusher';
import { PusherEvent } from '../connection/protocol/message-types';
import Metadata from './metadata';
import UrlStore from '../utils/url_store';
import {
  ChannelAuthorizationData,
  ChannelAuthorizationCallback
} from '../auth/options';
import { HTTPAuthError } from '../errors';

/** Provides base public channel interface with an event emitter.
 *
 * Emits:
 * - pusher:subscription_succeeded - after subscribing successfully
 * - other non-internal events
 *
 * @param {String} name
 * @param {Pusher} pusher
 */
export default class Channel extends EventsDispatcher {
  name: string;
  pusher: Pusher;
  subscribed: boolean;
  subscriptionPending: boolean;
  subscriptionCancelled: boolean;
  subscriptionCount: null;

  constructor(name: string, pusher: Pusher) {
    super(function(event, data) {
      Logger.debug('No callbacks on ' + name + ' for ' + event);
    });

    this.name = name;
    this.pusher = pusher;
    this.subscribed = false;
    this.subscriptionPending = false;
    this.subscriptionCancelled = false;
  }

  /** Skips authorization, since public channels don't require it.
   *
   * @param {Function} callback
   */
  authorize(socketId: string, callback: ChannelAuthorizationCallback) {
    return callback(null, { auth: '' });
  }

  /** Triggers an event */
  trigger(event: string, data: any) {
    if (event.indexOf('client-') !== 0) {
      throw new Errors.BadEventName(
        "Event '" + event + "' does not start with 'client-'"
      );
    }
    if (!this.subscribed) {
      var suffix = UrlStore.buildLogSuffix('triggeringClientEvents');
      Logger.warn(
        `Client event triggered before channel 'subscription_succeeded' event . ${suffix}`
      );
    }
    return this.pusher.send_event(event, data, this.name);
  }

  /** Signals disconnection to the channel. For internal use only. */
  disconnect() {
    this.subscribed = false;
    this.subscriptionPending = false;
  }

  /** Handles a PusherEvent. For internal use only.
   *
   * @param {PusherEvent} event
   */
  handleEvent(event: PusherEvent) {
    var eventName = event.event;
    var data = event.data;
    if (eventName === 'pusher_internal:subscription_succeeded') {
      this.handleSubscriptionSucceededEvent(event);
    } else if (eventName === 'pusher_internal:subscription_count') {
      this.handleSubscriptionCountEvent(event);
    } else if (eventName.indexOf('pusher_internal:') !== 0) {
      var metadata: Metadata = {};
      this.emit(eventName, data, metadata);
    }
  }

  handleSubscriptionSucceededEvent(event: PusherEvent) {
    this.subscriptionPending = false;
    this.subscribed = true;
    if (this.subscriptionCancelled) {
      this.pusher.unsubscribe(this.name);
    } else {
      this.emit('pusher:subscription_succeeded', event.data);
    }
  }

  handleSubscriptionCountEvent(event: PusherEvent) {
    if (event.data.subscription_count) {
      this.subscriptionCount = event.data.subscription_count;
    }

    this.emit('pusher:subscription_count', event.data);
  }

  /** Sends a subscription request. For internal use only. */
  subscribe() {
    if (this.subscribed) {
      return;
    }
    this.subscriptionPending = true;
    this.subscriptionCancelled = false;
    this.authorize(
      this.pusher.connection.socket_id,
      (error: Error | null, data: ChannelAuthorizationData) => {
        if (error) {
          this.subscriptionPending = false;
          // Why not bind to 'pusher:subscription_error' a level up, and log there?
          // Binding to this event would cause the warning about no callbacks being
          // bound (see constructor) to be suppressed, that's not what we want.
          Logger.error(error.toString());
          this.emit(
            'pusher:subscription_error',
            Object.assign(
              {},
              {
                type: 'AuthError',
                error: error.message
              },
              error instanceof HTTPAuthError ? { status: error.status } : {}
            )
          );
        } else {
          this.pusher.send_event('pusher:subscribe', {
            auth: data.auth,
            channel_data: data.channel_data,
            channel: this.name
          });
        }
      }
    );
  }

  /** Sends an unsubscription request. For internal use only. */
  unsubscribe() {
    this.subscribed = false;
    this.pusher.send_event('pusher:unsubscribe', {
      channel: this.name
    });
  }

  /** Cancels an in progress subscription. For internal use only. */
  cancelSubscription() {
    this.subscriptionCancelled = true;
  }

  /** Reinstates an in progress subscripiton. For internal use only. */
  reinstateSubscription() {
    this.subscriptionCancelled = false;
  }
}
