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

  A Detector used to test for up-going and down-going flanks

  Please note: The array contains flankLength + 1 measured currentDt's, thus flankLength number of flanks between them
  They are arranged that dataPoints[0] is the youngest, and dataPoints[flankLength] the oldest
*/
import loglevel from 'loglevel'
import { createMovingAverager } from './averager/MovingAverager.js'
const log = loglevel.getLogger('RowingEngine')

function createMovingFlankDetector (rowerSettings) {
  const angularDisplacementPerImpulse = (2.0 * Math.PI) / rowerSettings.numOfImpulsesPerRevolution
  const dirtyDataPoints = new Array(rowerSettings.flankLength + 1)
  dirtyDataPoints.fill(rowerSettings.maximumTimeBetweenImpulses)
  const cleanDataPoints = new Array(rowerSettings.flankLength + 1)
  cleanDataPoints.fill(rowerSettings.maximumTimeBetweenImpulses)
  const angularVelocity = new Array(rowerSettings.flankLength + 1)
  angularVelocity.fill(angularDisplacementPerImpulse / rowerSettings.minimumTimeBetweenImpulses)
  const angularAcceleration = new Array(rowerSettings.flankLength + 1)
  angularAcceleration.fill(0)
  const movingAverage = createMovingAverager(rowerSettings.smoothing, rowerSettings.maximumTimeBetweenImpulses)

  function pushValue (dataPoint) {
    // add the new dataPoint to the array, we have to move data points starting at the oldest ones
    let i = rowerSettings.flankLength
    while (i > 0) {
      // older data points are moved toward the higher numbers
      dirtyDataPoints[i] = dirtyDataPoints[i - 1]
      cleanDataPoints[i] = cleanDataPoints[i - 1]
      angularVelocity[i] = angularVelocity[i - 1]
      angularAcceleration[i] = angularAcceleration[i - 1]
      i = i - 1
    }
    dirtyDataPoints[0] = dataPoint

    // reduce noise in the measurements by applying some sanity checks
    // noise filter on the value of dataPoint: it should be within sane levels and should not deviate too much from the previous reading
    if (dataPoint < rowerSettings.minimumTimeBetweenImpulses || dataPoint > rowerSettings.maximumTimeBetweenImpulses) {
      // impulseTime is outside plausible ranges, so we assume it is close to the previous clean one
      log.debug(`noise filter corrected currentDt, ${dataPoint} was not between minimumTimeBetweenImpulses and maximumTimeBetweenImpulses, changed to ${cleanDataPoints[1]}`)
      dataPoint = cleanDataPoints[1]
    }

    // lets test if pushing this value would fit the curve we are looking for
    movingAverage.pushValue(dataPoint)

    if (movingAverage.getAverage() < (rowerSettings.maximumDownwardChange * cleanDataPoints[1]) || movingAverage.getAverage() > (rowerSettings.maximumUpwardChange * cleanDataPoints[1])) {
      // impulses are outside plausible ranges, so we assume it is close to the previous one
      log.debug(`noise filter corrected currentDt, ${dataPoint} was too much of an accelleration/decelleration with respect to ${movingAverage.getAverage()}, changed to previous value, ${cleanDataPoints[1]}`)
      movingAverage.replaceLastPushedValue(cleanDataPoints[1])
    }

    // determine the moving average, to reduce noise
    cleanDataPoints[0] = movingAverage.getAverage()

    // determine the derived data
    if (cleanDataPoints[0] > 0) {
      angularVelocity[0] = angularDisplacementPerImpulse / cleanDataPoints[0]
      angularAcceleration[0] = (angularVelocity[0] - angularVelocity[1]) / cleanDataPoints[0]
    } else {
      log.error('Impuls of 0 seconds encountered, this should not be possible (division by 0 prevented)')
      angularVelocity[0] = 0
      angularAcceleration[0] = 0
    }
  }

  function isFlywheelUnpowered () {
    let numberOfErrors = 0
    if (rowerSettings.naturalDeceleration < 0) {
      // A valid natural deceleration of the flywheel has been provided, this has to be maintained for a flank length
      // to count as an indication for an unpowered flywheel
      // Please note that angularAcceleration[] contains flank-information already, so we need to check from
      // rowerSettings.flankLength -1 until 0 flanks
      let i = rowerSettings.flankLength - 1
      while (i >= 0) {
        if (angularAcceleration[i] > rowerSettings.naturalDeceleration) {
          // There seems to be some power present, so we detected an error
          numberOfErrors = numberOfErrors + 1
        }
        i = i - 1
      }
    } else {
      // No valid natural deceleration has been provided, we rely on pure deceleration for recovery detection
      let i = rowerSettings.flankLength
      while (i > 0) {
        if (cleanDataPoints[i] >= cleanDataPoints[i - 1]) {
          // Oldest interval (dataPoints[i]) is larger than the younger one (datapoint[i-1], as the distance is
          // fixed, we are accelerating
          numberOfErrors = numberOfErrors + 1
        }
        i = i - 1
      }
    }
    if (numberOfErrors > rowerSettings.numberOfErrorsAllowed) {
      return false
    } else {
      return true
    }
  }

  function isFlywheelPowered () {
    let numberOfErrors = 0
    if (rowerSettings.naturalDeceleration < 0) {
      // A valid natural deceleration of the flywheel has been provided, this has to be consistently encountered
      // for a flank length to count as an indication of a powered flywheel
      // Please note that angularAcceleration[] contains flank-information already, so we need to check from
      // rowerSettings.flankLength -1 until 0 flanks
      let i = rowerSettings.flankLength - 1
      while (i >= 0) {
        if (angularAcceleration[i] < rowerSettings.naturalDeceleration) {
          // Some deceleration is below the natural deceleration, so we detected an error
          numberOfErrors = numberOfErrors + 1
        }
        i = i - 1
      }
    } else {
      // No valid natural deceleration of the flywheel has been provided, we rely on pure acceleration for stroke detection
      let i = rowerSettings.flankLength
      while (i > 1) {
        if (cleanDataPoints[i] < cleanDataPoints[i - 1]) {
          // Oldest interval (dataPoints[i]) is shorter than the younger one (datapoint[i-1], as the distance is fixed, we
          // discovered a deceleration
          numberOfErrors = numberOfErrors + 1
        }
        i = i - 1
      }
      if (cleanDataPoints[1] <= cleanDataPoints[0]) {
        // We handle the last measurement more specifically: at least the youngest measurement must be really accelerating
        // This prevents when the currentDt "flatlines" (i.e. error correction kicks in) a ghost-stroke is detected
        numberOfErrors = numberOfErrors + 1
      }
    }
    if (numberOfErrors > rowerSettings.numberOfErrorsAllowed) {
      return false
    } else {
      return true
    }
  }

  function timeToBeginOfFlank () {
    // We expect the curve to bend between dirtyDataPoints[rowerSettings.flankLength] and dirtyDataPoints[rowerSettings.flankLength+1],
    // as acceleration FOLLOWS the start of the pulling the handle, we assume it must have started before that
    let i = rowerSettings.flankLength
    let total = 0.0
    while (i >= 0) {
      total += dirtyDataPoints[i]
      i = i - 1
    }
    return total
  }

  function noImpulsesToBeginFlank () {
    return rowerSettings.flankLength
  }

  function impulseLengthAtBeginFlank () {
    // As this is fed into the speed calculation where small changes have big effects, and we typically use it when
    // the curve is in a plateau, we return the cleaned data and not the dirty data
    // Regardless of the way to determine the acceleration, cleanDataPoints[rowerSettings.flankLength] is always the
    // impulse at the beginning of the flank being investigated
    return cleanDataPoints[rowerSettings.flankLength]
  }

  function accelerationAtBeginOfFlank () {
    return angularAcceleration[rowerSettings.flankLength - 1]
  }

  return {
    pushValue,
    isFlywheelUnpowered,
    isFlywheelPowered,
    timeToBeginOfFlank,
    noImpulsesToBeginFlank,
    impulseLengthAtBeginFlank,
    accelerationAtBeginOfFlank
  }
}

export { createMovingFlankDetector }
