import { ExponentialBackoff, Websocket, WebsocketBuilder } from 'websocket-ts'
import { ActionClientMessage, ActionServerMessage, ConfirmServerMessage, HandshakeClientMessage, HandshakeServerMessage, ServerMessage, KetchupSynced } from 'ketchup-common'
import EventEmitter from 'events'

export default class KetchupClient<State, Action> extends EventEmitter {
  readonly ws: Websocket
  synced?: KetchupSynced<State, Action>
  clientId?: number
  lastTimestamp?: number

  constructor(
    url: string,
    readonly reducer: (state: State, action: Action) => State,
    private readonly timeSource = Date.now
  ) {
    super()
    this.ws = new WebsocketBuilder(url)
      .onClose(() => {
        this.synced = undefined
        this.clientId = undefined
        this.lastTimestamp = undefined
      })
      .onOpen(ws => {
        ws.send(JSON.stringify({
          message: 'handshake',
          timestamp: timeSource(),
        } as HandshakeClientMessage))
      })
      .onMessage((_, event) => {
        this.onMessage(JSON.parse(event.data))
      })
      .withBackoff(new ExponentialBackoff(100, 7))
      .build()
  }

  private onMessage(msg: ServerMessage<State, Action>) {
    switch (msg.message) {
      case 'handshake':
        const hMsg = msg as HandshakeServerMessage<State, Action>
        this.clientId = hMsg.clientId
        this.synced = new KetchupSynced(hMsg.confState, hMsg.confBefore, this.reducer, hMsg.unconfActions)
        this.synced.on('projection', proj => this.emit('projection', proj))
        this.emit('projection', this.synced.projState)
        break

      case 'action':
        this.synced!.tryProcess(msg as ActionServerMessage<Action>)
        break

      case 'confirm':
        this.synced!.confirmBefore((msg as ConfirmServerMessage).confBefore)
        break
      
      default:
        throw new Error(`Unknown message from server: ${msg}`)
    }
  }

  dispatch(action: Action, timestamp = this.timeSource()) {
    // In case of timestamp collision, shift event 1ms into future until timestamp is novel.
    // TODO: Better handling of events being submitted out of timestamp order
    if (this.lastTimestamp !== undefined && timestamp <= this.lastTimestamp) {
      timestamp = this.lastTimestamp + 1
    }

    if (this.synced?.tryProcess({ timestamp, action, clientId: this.clientId! })) {
      this.lastTimestamp = timestamp
      this.ws.send(JSON.stringify({
        message: 'action',
        action,
        timestamp,
      } as ActionClientMessage<Action>))
    }
  }
}
