'use strict'
/*
  Open Rowing Monitor, https://github.com/laberning/openrowingmonitor

  The connected Central can remotly control some parameters or our rowing monitor via this Control Point

  So far tested on:
    - Fulgaz: uses setIndoorBikeSimulationParameters
    - Zwift: uses startOrResume and setIndoorBikeSimulationParameters
*/
import bleno from '@abandonware/bleno'
import log from 'loglevel'

// see https://www.bluetooth.com/specifications/specs/fitness-machine-service-1-0 for details
const ControlPointOpCode = {
  requestControl: 0x00,
  reset: 0x01,
  setTargetSpeed: 0x02,
  setTargetInclincation: 0x03,
  setTargetResistanceLevel: 0x04,
  setTargetPower: 0x05,
  setTargetHeartRate: 0x06,
  startOrResume: 0x07,
  stopOrPause: 0x08,
  setTargetedExpendedEnergy: 0x09,
  setTargetedNumberOfSteps: 0x0A,
  setTargetedNumberOfStrides: 0x0B,
  setTargetedDistance: 0x0C,
  setTargetedTrainingTime: 0x0D,
  setTargetedTimeInTwoHeartRateZones: 0x0E,
  setTargetedTimeInThreeHeartRateZones: 0x0F,
  setTargetedTimeInFiveHeartRateZones: 0x10,
  setIndoorBikeSimulationParameters: 0x11,
  setWheelCircumference: 0x12,
  spinDownControl: 0x13,
  setTargetedCadence: 0x14,
  responseCode: 0x80
}

const ResultCode = {
  reserved: 0x00,
  success: 0x01,
  opCodeNotSupported: 0x02,
  invalidParameter: 0x03,
  operationFailed: 0x04,
  controlNotPermitted: 0x05
}

export default class FitnessMachineControlPointCharacteristic extends bleno.Characteristic {
  constructor (controlPointCallback) {
    super({
      // Fitness Machine Control Point
      uuid: '2AD9',
      value: null,
      properties: ['write']
    })

    this.controlled = false
    if (!controlPointCallback) { throw new Error('controlPointCallback required') }
    this.controlPointCallback = controlPointCallback
  }

  // Central sends a command to the Control Point
  // todo: handle offset and withoutResponse properly
  onWriteRequest (data, offset, withoutResponse, callback) {
    const opCode = data.readUInt8(0)
    switch (opCode) {
      case ControlPointOpCode.requestControl:
        if (!this.controlled) {
          if (this.controlPointCallback({ name: 'requestControl' })) {
            log.debug('requestControl sucessful')
            this.controlled = true
            callback(this.buildResponse(opCode, ResultCode.success))
          } else {
            callback(this.buildResponse(opCode, ResultCode.operationFailed))
          }
        } else {
          callback(this.buildResponse(opCode, ResultCode.controlNotPermitted))
        }
        break

      case ControlPointOpCode.reset:
        this.handleSimpleCommand(ControlPointOpCode.reset, 'reset', callback)
        // as per spec the reset command shall also reset the control
        this.controlled = false
        break

      case ControlPointOpCode.startOrResume:
        this.handleSimpleCommand(ControlPointOpCode.startOrResume, 'startOrResume', callback)
        break

      case ControlPointOpCode.stopOrPause: {
        const controlParameter = data.readUInt8(1)
        if (controlParameter === 1) {
          this.handleSimpleCommand(ControlPointOpCode.stopOrPause, 'stop', callback)
        } else if (controlParameter === 2) {
          this.handleSimpleCommand(ControlPointOpCode.stopOrPause, 'pause', callback)
        } else {
          log.error(`stopOrPause with invalid controlParameter: ${controlParameter}`)
        }
        break
      }

      // todo: Most tested bike apps use these to simulate a bike ride. Not sure how we can use these in our rower
      // since there is no adjustable resistance on the rowing machine
      case ControlPointOpCode.setIndoorBikeSimulationParameters: {
        const windspeed = data.readInt16LE(1) * 0.001
        const grade = data.readInt16LE(3) * 0.01
        const crr = data.readUInt8(5) * 0.0001
        const cw = data.readUInt8(6) * 0.01
        if (this.controlPointCallback({ name: 'setIndoorBikeSimulationParameters', value: { windspeed, grade, crr, cw } })) {
          callback(this.buildResponse(opCode, ResultCode.success))
        } else {
          callback(this.buildResponse(opCode, ResultCode.operationFailed))
        }
        break
      }

      default:
        log.info(`opCode ${opCode} is not supported`)
        callback(this.buildResponse(opCode, ResultCode.opCodeNotSupported))
    }
  }

  handleSimpleCommand (opCode, opName, callback) {
    if (this.controlled) {
      if (this.controlPointCallback({ name: opName })) {
        const response = this.buildResponse(opCode, ResultCode.success)
        callback(response)
      } else {
        callback(this.buildResponse(opCode, ResultCode.operationFailed))
      }
    } else {
      log.info(`initating command '${opName}' requires 'requestControl'`)
      callback(this.buildResponse(opCode, ResultCode.controlNotPermitted))
    }
  }

  // build the response message as defined by the spec
  buildResponse (opCode, resultCode) {
    const buffer = Buffer.alloc(3)
    buffer.writeUInt8(0x80, 0)
    buffer.writeUInt8(opCode, 1)
    buffer.writeUInt8(resultCode, 2)
    return buffer
  }
}
