import { DEFAULT_TIMEOUT } from '../lib/constants'
import type RealtimeChannel from '../RealtimeChannel'

export default class Push {
  sent: boolean = false
  timeoutTimer: number | undefined = undefined
  ref: string = ''
  receivedResp: {
    status: string
    response: { [key: string]: any }
  } | null = null
  recHooks: {
    status: string
    callback: Function
  }[] = []
  refEvent: string | null = null
  rateLimited: boolean = false

  /**
   * Initializes the Push
   *
   * @param channel The Channel
   * @param event The event, for example `"phx_join"`
   * @param payload The payload, for example `{user_id: 123}`
   * @param timeout The push timeout in milliseconds
   */
  constructor(
    public channel: RealtimeChannel,
    public event: string,
    public payload: { [key: string]: any } = {},
    public timeout: number = DEFAULT_TIMEOUT
  ) {}

  resend(timeout: number) {
    this.timeout = timeout
    this._cancelRefEvent()
    this.ref = ''
    this.refEvent = null
    this.receivedResp = null
    this.sent = false
    this.send()
  }

  send() {
    if (this._hasReceived('timeout')) {
      return
    }
    this.startTimeout()
    this.sent = true
    const status = this.channel.socket.push({
      topic: this.channel.topic,
      event: this.event,
      payload: this.payload,
      ref: this.ref,
      join_ref: this.channel._joinRef(),
    })
    if (status === 'rate limited') {
      this.rateLimited = true
    }
  }

  updatePayload(payload: { [key: string]: any }): void {
    this.payload = { ...this.payload, ...payload }
  }

  receive(status: string, callback: Function) {
    if (this._hasReceived(status)) {
      callback(this.receivedResp?.response)
    }

    this.recHooks.push({ status, callback })
    return this
  }

  startTimeout() {
    if (this.timeoutTimer) {
      return
    }
    this.ref = this.channel.socket._makeRef()
    this.refEvent = this.channel._replyEventName(this.ref)

    const callback = (payload: any) => {
      this._cancelRefEvent()
      this._cancelTimeout()
      this.receivedResp = payload
      this._matchReceive(payload)
    }

    this.channel._on(this.refEvent, {}, callback)

    this.timeoutTimer = <any>setTimeout(() => {
      this.trigger('timeout', {})
    }, this.timeout)
  }

  trigger(status: string, response: any) {
    if (this.refEvent)
      this.channel._trigger(this.refEvent, { status, response })
  }

  destroy() {
    this._cancelRefEvent()
    this._cancelTimeout()
  }

  private _cancelRefEvent() {
    if (!this.refEvent) {
      return
    }

    this.channel._off(this.refEvent, {})
  }

  private _cancelTimeout() {
    clearTimeout(this.timeoutTimer)
    this.timeoutTimer = undefined
  }

  private _matchReceive({
    status,
    response,
  }: {
    status: string
    response: Function
  }) {
    this.recHooks
      .filter((h) => h.status === status)
      .forEach((h) => h.callback(response))
  }

  private _hasReceived(status: string) {
    return this.receivedResp && this.receivedResp.status === status
  }
}
