/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.common

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import java.util.concurrent.TimeUnit

/** Listens for the user shaking their phone. Allocation-less once it starts listening. */
public class ShakeDetector
@JvmOverloads
constructor(private val shakeListener: ShakeListener, private val minNumShakes: Int = 1) :
    SensorEventListener {

  private var accelerationX = 0f
  private var accelerationY = 0f
  private var accelerationZ = 0f

  public fun interface ShakeListener {
    public fun onShake()
  }

  private var sensorManager: SensorManager? = null
  private var lastTimestamp: Long = 0
  private var numShakes = 0
  private var lastShakeTimestamp: Long = 0

  /** Start listening for shakes. */
  public fun start(manager: SensorManager): Unit {
    val accelerometer = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) ?: return
    sensorManager = manager
    lastTimestamp = -1
    manager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI)
    lastShakeTimestamp = 0
    reset()
  }

  /** Stop listening for shakes. */
  public fun stop(): Unit {
    sensorManager?.unregisterListener(this)
    sensorManager = null
  }

  /** Reset all variables used to keep track of number of shakes recorded. */
  private fun reset() {
    numShakes = 0
    accelerationX = 0f
    accelerationY = 0f
    accelerationZ = 0f
  }

  /**
   * Determine if acceleration applied to sensor is large enough to count as a rage shake.
   *
   * @param a acceleration in x, y, or z applied to the sensor
   * @return true if the magnitude of the force exceeds the minimum required amount of force. false
   *   otherwise.
   */
  private fun atLeastRequiredForce(a: Float): Boolean = Math.abs(a) > REQUIRED_FORCE

  /**
   * Save data about last shake
   *
   * @param timestamp (ns) of last sensor event
   */
  private fun recordShake(timestamp: Long) {
    lastShakeTimestamp = timestamp
    numShakes++
  }

  override fun onSensorChanged(sensorEvent: SensorEvent) {
    if (sensorEvent.timestamp - lastTimestamp < MIN_TIME_BETWEEN_SAMPLES_NS) {
      return
    }
    val ax = sensorEvent.values[0]
    val ay = sensorEvent.values[1]
    val az = sensorEvent.values[2] - SensorManager.GRAVITY_EARTH
    lastTimestamp = sensorEvent.timestamp
    when {
      atLeastRequiredForce(ax) && ax * accelerationX <= 0 -> {
        recordShake(sensorEvent.timestamp)
        accelerationX = ax
      }
      atLeastRequiredForce(ay) && ay * accelerationY <= 0 -> {
        recordShake(sensorEvent.timestamp)
        accelerationY = ay
      }
      atLeastRequiredForce(az) && az * accelerationZ <= 0 -> {
        recordShake(sensorEvent.timestamp)
        accelerationZ = az
      }
    }
    maybeDispatchShake(sensorEvent.timestamp)
  }

  override fun onAccuracyChanged(sensor: Sensor, i: Int): Unit = Unit

  private fun maybeDispatchShake(currentTimestamp: Long) {
    if (numShakes >= 8 * minNumShakes) {
      reset()
      shakeListener.onShake()
    }
    if (currentTimestamp - lastShakeTimestamp > SHAKING_WINDOW_NS) {
      reset()
    }
  }
}

// Collect sensor data in this interval (nanoseconds)
private val MIN_TIME_BETWEEN_SAMPLES_NS = TimeUnit.NANOSECONDS.convert(20, TimeUnit.MILLISECONDS)

// Number of nanoseconds to listen for and count shakes (nanoseconds)
private val SHAKING_WINDOW_NS = TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS).toFloat()

// Required force to constitute a rage shake. Need to multiply gravity by 1.33 because a rage
// shake in one direction should have more force than just the magnitude of free fall.
private const val REQUIRED_FORCE = SensorManager.GRAVITY_EARTH * 1.33f
