// -----------------------
// TypeScript [2.6.2]
// -----------------------

import MSeriesReceiverParser from './mSeriesReceiverParser'

const RECEIVER_VENDOR_ID = 0x0483
const RECEIVER_PRODUCT_ID = 0x5740
const DEVICE_NAME = 'USB Multi-Bike Reciever'

export class Receiver {
  callback: ((broadcast: Broadcast) => void)
  targetDevice: Device | null

  constructor (callback: ((broadcast: Broadcast) => void)) {
    this.callback = callback
    this.targetDevice = null

    navigator.usb.addEventListener('connect', event => { this.attached(event) })
    navigator.usb.addEventListener('disconnect', event => { this.detached(event) })

    this.checkDevices()
  }

  async checkDevices () {
    let devices = await navigator.usb.getDevices()
    devices.forEach(device => {
      if (this.matchesTarget(device)) {
        console.log(DEVICE_NAME + ': Already Connected')
        this.connect(device)
      }
    })
  }

  async requestPermission () {
    if (!this.targetDevice) {
      try {
        let device = await navigator.usb.requestDevice({
          filters: [{
            vendorId: RECEIVER_VENDOR_ID,
            productId: RECEIVER_PRODUCT_ID
          }]
        })
        this.connect(device)
      } catch (error) {
        console.log(DEVICE_NAME + ': Permission Denied')
      }
    }
  }

  close () {
    if (this.targetDevice) {
      this.targetDevice.disconnect()
      this.targetDevice = null
    }
  }

  private attached (event: WebUSB.ConnectionEvent) {
    if (this.matchesTarget(event.device)) {
      console.log(DEVICE_NAME + ': Connected')
      this.connect(event.device)
    }
  }

  private detached (event: WebUSB.ConnectionEvent) {
    if (this.matchesTarget(event.device) && this.targetDevice && this.targetDevice.isSameDevice(event.device)) {
      console.log(DEVICE_NAME + ': Disconnected')
      this.close()
    }
  }

  private connect (device: WebUSB.Device) {
    this.targetDevice = new Device(device, this.callback)
  }

  private matchesTarget (device: WebUSB.Device) {
    return device.vendorId === RECEIVER_VENDOR_ID &&
      device.productId === RECEIVER_PRODUCT_ID
  }
}

export default class Device {
  bulkInEndpoint: WebUSB.Endpoint | null
  continueTransfer: boolean
  callback: ((broadcast: Broadcast) => void)
  coreDevice: WebUSB.Device

  constructor (device: WebUSB.Device, callback: ((broadcast: Broadcast) => void)) {
    this.bulkInEndpoint = null
    this.continueTransfer = false
    this.callback = callback
    this.coreDevice = device

    this.initialize()
  }

  /*****************************************
  *  Exposed Controls
  *****************************************/

  isSameDevice (device: WebUSB.Device) {
    return device.serialNumber === this.coreDevice.serialNumber
  }

  async disconnect () {
    try {
      this.stopTransfer()
      this.coreDevice.close()
      if (process.env.NODE_ENV !== 'production') {
        console.log('Device closed')
      }
    } catch (error) {
      if (process.env.NODE_ENV !== 'production') {
        console.error('Error closing device\n', error)
      }
    }
  }

  /*****************************************
  *  Control Flow
  *****************************************/

  async initialize () {
    await this.coreDevice.open()
    await this.mapEndpoints()
    this.startTransfer()
  }

  /*****************************************
  *  Interface and Endpoints
  *****************************************/

  async mapEndpoints () {
    let targetConfiguration: WebUSB.Configuration | null = null
    let targetInterface: WebUSB.USBInterface | null = null
    this.coreDevice.configurations.forEach(configuration => {
      configuration.interfaces.forEach(usbInterface => {
        usbInterface.alternates.forEach(alternate => {
          alternate.endpoints.forEach(endpoint => {
            if (endpoint.direction === 'in' && endpoint.type === 'bulk') {
              targetConfiguration = configuration
              targetInterface = usbInterface
              this.bulkInEndpoint = endpoint
            }
          })
        })
      })
    })

    if (!targetInterface || !targetConfiguration) {
      throw new Error('Device interfaces unavailable')
    }

    try {
      if (this.coreDevice.configuration === null || this.coreDevice.configuration.configurationValue !== targetConfiguration!.configurationValue) {
        await this.coreDevice.selectConfiguration(targetConfiguration!.configurationValue)
      }
      await this.coreDevice.claimInterface(targetInterface!.interfaceNumber)
    } catch (error) {
      console.error(error)
    }
  }

  /*****************************************
  *  Transfer
  *****************************************/

  async startTransfer () {
    if (!this.bulkInEndpoint) {
      throw new Error('Device endpoint not assigned')
    }

    this.continueTransfer = true

    try {
      while (this.continueTransfer) {
        let result = await this.coreDevice.transferIn(this.bulkInEndpoint.endpointNumber, 4096)

        if (result.data && result.data.byteLength > 0) {
          this.parse(result.data)
        }

        if (result.status === 'stall') {
          await this.coreDevice.clearHalt('in', this.bulkInEndpoint.endpointNumber)
        }
      }
    } catch (error) {
      this.stopTransfer()
    }
  }

  stopTransfer () {
    this.continueTransfer = false
  }

  /*****************************************
  *  Parse
  *****************************************/

  parse (data: DataView) {
    let rawStream = new Uint8Array(data.buffer)
    let segments = String.fromCharCode.apply(null, rawStream).split(' ')
    let firstSegmentValue = parseInt(segments[0], 10)
    if (firstSegmentValue <= 200) {
      let swValue = parseFloat(segments[6])
      if (swValue >= 6.0 && swValue < 7.0) {
        try {
          let broadcast = MSeriesReceiverParser(segments)
          this.callback(broadcast)
        } catch (error) {
          console.error('M Series parse error\n', error)
        }
      } else if (swValue >= 8.0 && swValue < 9.0) {
        console.error('Incorrect device connected')
      }
    }
  }
}