package versioned.host.exp.exponent.modules.api.components.gesturehandler

import android.view.MotionEvent
import android.view.MotionEvent.PointerCoords
import android.view.MotionEvent.PointerProperties
import android.view.View
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.bridge.WritableArray
import com.facebook.react.uimanager.PixelUtil
import versioned.host.exp.exponent.modules.api.components.gesturehandler.react.RNGestureHandlerTouchEvent
import java.lang.IllegalStateException
import java.util.*

open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestureHandlerT>> {
  private val trackedPointerIDs = IntArray(MAX_POINTERS_COUNT)
  private var trackedPointersIDsCount = 0
  var tag = 0
  var view: View? = null
    private set
  var state = STATE_UNDETERMINED
    private set
  var x = 0f
    private set
  var y = 0f
    private set
  var isWithinBounds = false
    private set
  var isEnabled = true
    private set
  var usesDeviceEvents = false

  var changedTouchesPayload: WritableArray? = null
    private set
  var allTouchesPayload: WritableArray? = null
    private set
  var touchEventType = RNGestureHandlerTouchEvent.EVENT_UNDETERMINED
    private set
  var trackedPointersCount = 0
    private set
  private val trackedPointers: Array<PointerData?> = Array(MAX_POINTERS_COUNT) { null }
  var needsPointerData = false

  private var hitSlop: FloatArray? = null
  var eventCoalescingKey: Short = 0
    private set
  var lastAbsolutePositionX = 0f
    private set
  var lastAbsolutePositionY = 0f
    private set

  private var manualActivation = false

  private var lastEventOffsetX = 0f
  private var lastEventOffsetY = 0f
  private var shouldCancelWhenOutside = false
  var numberOfPointers = 0
    private set
  private var orchestrator: GestureHandlerOrchestrator? = null
  private var onTouchEventListener: OnTouchEventListener? = null
  private var interactionController: GestureHandlerInteractionController? = null

  @Suppress("UNCHECKED_CAST")
  protected fun self(): ConcreteGestureHandlerT = this as ConcreteGestureHandlerT

  protected inline fun applySelf(block: ConcreteGestureHandlerT.() -> Unit): ConcreteGestureHandlerT =
    self().apply { block() }

  // set and accessed only by the orchestrator
  var activationIndex = 0

  // set and accessed only by the orchestrator
  var isActive = false

  // set and accessed only by the orchestrator
  var isAwaiting = false

  open fun dispatchStateChange(newState: Int, prevState: Int) {
    onTouchEventListener?.onStateChange(self(), newState, prevState)
  }

  open fun dispatchHandlerUpdate(event: MotionEvent) {
    onTouchEventListener?.onHandlerUpdate(self(), event)
  }

  open fun dispatchTouchEvent() {
    if (changedTouchesPayload != null) {
      onTouchEventListener?.onTouchEvent(self())
    }
  }

  open fun resetConfig() {
    needsPointerData = false
    manualActivation = false
    shouldCancelWhenOutside = false
    isEnabled = true
    hitSlop = null
  }

  fun hasCommonPointers(other: GestureHandler<*>): Boolean {
    for (i in trackedPointerIDs.indices) {
      if (trackedPointerIDs[i] != -1 && other.trackedPointerIDs[i] != -1) {
        return true
      }
    }
    return false
  }

  fun setShouldCancelWhenOutside(shouldCancelWhenOutside: Boolean): ConcreteGestureHandlerT =
    applySelf { this.shouldCancelWhenOutside = shouldCancelWhenOutside }

  fun setEnabled(enabled: Boolean): ConcreteGestureHandlerT = applySelf {
    // Don't cancel handler when not changing the value of the isEnabled, executing it always caused
    // handlers to be cancelled on re-render because that's the moment when the config is updated.
    // If the enabled prop "changed" from true to true the handler would get cancelled.
    if (view != null && isEnabled != enabled) {
      // If view is set then handler is in "active" state. In that case we want to "cancel" handler
      // when it changes enabled state so that it gets cleared from the orchestrator
      UiThreadUtil.runOnUiThread { cancel() }
    }
    isEnabled = enabled
  }

  fun setManualActivation(manualActivation: Boolean): ConcreteGestureHandlerT =
    applySelf { this.manualActivation = manualActivation }

  fun setHitSlop(
    leftPad: Float,
    topPad: Float,
    rightPad: Float,
    bottomPad: Float,
    width: Float,
    height: Float,
  ): ConcreteGestureHandlerT = applySelf {
    if (hitSlop == null) {
      hitSlop = FloatArray(6)
    }
    hitSlop!![HIT_SLOP_LEFT_IDX] = leftPad
    hitSlop!![HIT_SLOP_TOP_IDX] = topPad
    hitSlop!![HIT_SLOP_RIGHT_IDX] = rightPad
    hitSlop!![HIT_SLOP_BOTTOM_IDX] = bottomPad
    hitSlop!![HIT_SLOP_WIDTH_IDX] = width
    hitSlop!![HIT_SLOP_HEIGHT_IDX] = height
    require(!(hitSlopSet(width) && hitSlopSet(leftPad) && hitSlopSet(rightPad))) { "Cannot have all of left, right and width defined" }
    require(!(hitSlopSet(width) && !hitSlopSet(leftPad) && !hitSlopSet(rightPad))) { "When width is set one of left or right pads need to be defined" }
    require(!(hitSlopSet(height) && hitSlopSet(bottomPad) && hitSlopSet(topPad))) { "Cannot have all of top, bottom and height defined" }
    require(!(hitSlopSet(height) && !hitSlopSet(bottomPad) && !hitSlopSet(topPad))) { "When height is set one of top or bottom pads need to be defined" }
  }

  fun setHitSlop(padding: Float): ConcreteGestureHandlerT {
    return setHitSlop(padding, padding, padding, padding, HIT_SLOP_NONE, HIT_SLOP_NONE)
  }

  fun setInteractionController(controller: GestureHandlerInteractionController?): ConcreteGestureHandlerT =
    applySelf { interactionController = controller }

  fun prepare(view: View?, orchestrator: GestureHandlerOrchestrator?) {
    check(!(this.view != null || this.orchestrator != null)) { "Already prepared or hasn't been reset" }
    Arrays.fill(trackedPointerIDs, -1)
    trackedPointersIDsCount = 0
    state = STATE_UNDETERMINED
    this.view = view
    this.orchestrator = orchestrator
  }

  private fun findNextLocalPointerId(): Int {
    var localPointerId = 0
    while (localPointerId < trackedPointersIDsCount) {
      var i = 0
      while (i < trackedPointerIDs.size) {
        if (trackedPointerIDs[i] == localPointerId) {
          break
        }
        i++
      }
      if (i == trackedPointerIDs.size) {
        return localPointerId
      }
      localPointerId++
    }
    return localPointerId
  }

  fun startTrackingPointer(pointerId: Int) {
    if (trackedPointerIDs[pointerId] == -1) {
      trackedPointerIDs[pointerId] = findNextLocalPointerId()
      trackedPointersIDsCount++
    }
  }

  fun stopTrackingPointer(pointerId: Int) {
    if (trackedPointerIDs[pointerId] != -1) {
      trackedPointerIDs[pointerId] = -1
      trackedPointersIDsCount--
    }
  }

  private fun needAdapt(event: MotionEvent): Boolean {
    if (event.pointerCount != trackedPointersIDsCount) {
      return true
    }

    for (i in trackedPointerIDs.indices) {
      val trackedPointer = trackedPointerIDs[i]
      if (trackedPointer != -1 && trackedPointer != i) {
        return true
      }
    }
    return false
  }

  private fun adaptEvent(event: MotionEvent): MotionEvent {
    if (!needAdapt(event)) {
      return event
    }
    var action = event.actionMasked
    var actionIndex = -1
    if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
      actionIndex = event.actionIndex
      val actionPointer = event.getPointerId(actionIndex)
      action = if (trackedPointerIDs[actionPointer] != -1) {
        if (trackedPointersIDsCount == 1) MotionEvent.ACTION_DOWN else MotionEvent.ACTION_POINTER_DOWN
      } else {
        MotionEvent.ACTION_MOVE
      }
    } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
      actionIndex = event.actionIndex
      val actionPointer = event.getPointerId(actionIndex)
      action = if (trackedPointerIDs[actionPointer] != -1) {
        if (trackedPointersIDsCount == 1) MotionEvent.ACTION_UP else MotionEvent.ACTION_POINTER_UP
      } else {
        MotionEvent.ACTION_MOVE
      }
    }
    initPointerProps(trackedPointersIDsCount)
    var count = 0
    val oldX = event.x
    val oldY = event.y
    event.setLocation(event.rawX, event.rawY)
    var index = 0
    val size = event.pointerCount
    while (index < size) {
      val origPointerId = event.getPointerId(index)
      if (trackedPointerIDs[origPointerId] != -1) {
        event.getPointerProperties(index, pointerProps[count])
        pointerProps[count]!!.id = trackedPointerIDs[origPointerId]
        event.getPointerCoords(index, pointerCoords[count])
        if (index == actionIndex) {
          action = action or (count shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
        }
        count++
      }
      index++
    }

    // introduced in 1.11.0, remove if crashes are not reported
    if (pointerProps.isEmpty() || pointerCoords.isEmpty()) {
      throw IllegalStateException("pointerCoords.size=${pointerCoords.size}, pointerProps.size=${pointerProps.size}")
    }

    val result: MotionEvent
    try {
      result = MotionEvent.obtain(
        event.downTime,
        event.eventTime,
        action,
        count,
        pointerProps, /* props are copied and hence it is safe to use static array here */
        pointerCoords, /* same applies to coords */
        event.metaState,
        event.buttonState,
        event.xPrecision,
        event.yPrecision,
        event.deviceId,
        event.edgeFlags,
        event.source,
        event.flags
      )
    } catch (e: IllegalArgumentException) {
      throw AdaptEventException(this, event, e)
    }
    event.setLocation(oldX, oldY)
    result.setLocation(oldX, oldY)
    return result
  }

  // exception to help debug https://github.com/software-mansion/react-native-gesture-handler/issues/1188
  class AdaptEventException(
    handler: GestureHandler<*>,
    event: MotionEvent,
    e: IllegalArgumentException
  ) : Exception(
    """
    handler: ${handler::class.simpleName}
    state: ${handler.state}
    view: ${handler.view}
    orchestrator: ${handler.orchestrator}
    isEnabled: ${handler.isEnabled}
    isActive: ${handler.isActive}
    isAwaiting: ${handler.isAwaiting}
    trackedPointersCount: ${handler.trackedPointersCount}
    trackedPointers: ${handler.trackedPointerIDs.joinToString(separator = ", ")}
    while handling event: $event
    """.trimIndent(),
    e
  )

  fun handle(origEvent: MotionEvent) {
    if (!isEnabled ||
      state == STATE_CANCELLED ||
      state == STATE_FAILED ||
      state == STATE_END ||
      trackedPointersIDsCount < 1
    ) {
      return
    }
    val event = adaptEvent(origEvent)
    x = event.x
    y = event.y
    numberOfPointers = event.pointerCount
    isWithinBounds = isWithinBounds(view, x, y)
    if (shouldCancelWhenOutside && !isWithinBounds) {
      if (state == STATE_ACTIVE) {
        cancel()
      } else if (state == STATE_BEGAN) {
        fail()
      }
      return
    }
    lastAbsolutePositionX = GestureUtils.getLastPointerX(event, true)
    lastAbsolutePositionY = GestureUtils.getLastPointerY(event, true)
    lastEventOffsetX = event.rawX - event.x
    lastEventOffsetY = event.rawY - event.y
    onHandle(event)
    if (event != origEvent) {
      event.recycle()
    }
  }

  private fun dispatchTouchDownEvent(event: MotionEvent) {
    changedTouchesPayload = null
    touchEventType = RNGestureHandlerTouchEvent.EVENT_TOUCH_DOWN
    val pointerId = event.getPointerId(event.actionIndex)
    val offsetX = event.rawX - event.x
    val offsetY = event.rawY - event.y

    trackedPointers[pointerId] = PointerData(
      pointerId,
      event.getX(event.actionIndex),
      event.getY(event.actionIndex),
      event.getX(event.actionIndex) + offsetX,
      event.getY(event.actionIndex) + offsetY,
    )
    trackedPointersCount++
    addChangedPointer(trackedPointers[pointerId]!!)
    extractAllPointersData()

    dispatchTouchEvent()
  }

  private fun dispatchTouchUpEvent(event: MotionEvent) {
    extractAllPointersData()
    changedTouchesPayload = null
    touchEventType = RNGestureHandlerTouchEvent.EVENT_TOUCH_UP
    val pointerId = event.getPointerId(event.actionIndex)
    val offsetX = event.rawX - event.x
    val offsetY = event.rawY - event.y

    trackedPointers[pointerId] = PointerData(
      pointerId,
      event.getX(event.actionIndex),
      event.getY(event.actionIndex),
      event.getX(event.actionIndex) + offsetX,
      event.getY(event.actionIndex) + offsetY,
    )
    addChangedPointer(trackedPointers[pointerId]!!)
    trackedPointers[pointerId] = null
    trackedPointersCount--

    dispatchTouchEvent()
  }

  private fun dispatchTouchMoveEvent(event: MotionEvent) {
    changedTouchesPayload = null
    touchEventType = RNGestureHandlerTouchEvent.EVENT_TOUCH_MOVE
    val offsetX = event.rawX - event.x
    val offsetY = event.rawY - event.y
    var pointersAdded = 0

    for (i in 0 until event.pointerCount) {
      val pointerId = event.getPointerId(i)
      val pointer = trackedPointers[pointerId] ?: continue

      if (pointer.x != event.getX(i) || pointer.y != event.getY(i)) {
        pointer.x = event.getX(i)
        pointer.y = event.getY(i)
        pointer.absoluteX = event.getX(i) + offsetX
        pointer.absoluteY = event.getY(i) + offsetY

        addChangedPointer(pointer)
        pointersAdded++
      }
    }

    // only data about pointers that have changed their position is sent, it makes no sense to send
    // an empty move event (especially when this method is called during down/up event and there is
    // only info about one pointer)
    if (pointersAdded > 0) {
      extractAllPointersData()
      dispatchTouchEvent()
    }
  }

  fun updatePointerData(event: MotionEvent) {
    if (event.actionMasked == MotionEvent.ACTION_DOWN || event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) {
      dispatchTouchDownEvent(event)
      dispatchTouchMoveEvent(event)
    } else if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_POINTER_UP) {
      dispatchTouchMoveEvent(event)
      dispatchTouchUpEvent(event)
    } else if (event.actionMasked == MotionEvent.ACTION_MOVE) {
      dispatchTouchMoveEvent(event)
    }
  }

  private fun extractAllPointersData() {
    allTouchesPayload = null

    for (pointerData in trackedPointers) {
      if (pointerData != null) {
        addPointerToAll(pointerData)
      }
    }
  }

  private fun cancelPointers() {
    touchEventType = RNGestureHandlerTouchEvent.EVENT_TOUCH_CANCELLED
    changedTouchesPayload = null
    extractAllPointersData()

    for (pointer in trackedPointers) {
      pointer?.let {
        addChangedPointer(it)
      }
    }

    trackedPointersCount = 0
    trackedPointers.fill(null)

    dispatchTouchEvent()
  }

  private fun addChangedPointer(pointerData: PointerData) {
    if (changedTouchesPayload == null) {
      changedTouchesPayload = Arguments.createArray()
    }

    changedTouchesPayload!!.pushMap(createPointerData(pointerData))
  }

  private fun addPointerToAll(pointerData: PointerData) {
    if (allTouchesPayload == null) {
      allTouchesPayload = Arguments.createArray()
    }

    allTouchesPayload!!.pushMap(createPointerData(pointerData))
  }

  private fun createPointerData(pointerData: PointerData) = Arguments.createMap().apply {
    putInt("id", pointerData.pointerId)
    putDouble("x", PixelUtil.toDIPFromPixel(pointerData.x).toDouble())
    putDouble("y", PixelUtil.toDIPFromPixel(pointerData.y).toDouble())
    putDouble("absoluteX", PixelUtil.toDIPFromPixel(pointerData.absoluteX).toDouble())
    putDouble("absoluteY", PixelUtil.toDIPFromPixel(pointerData.absoluteY).toDouble())
  }

  fun consumeChangedTouchesPayload(): WritableArray? {
    val result = changedTouchesPayload
    changedTouchesPayload = null
    return result
  }

  fun consumeAllTouchesPayload(): WritableArray? {
    val result = allTouchesPayload
    allTouchesPayload = null
    return result
  }

  private fun moveToState(newState: Int) {
    UiThreadUtil.assertOnUiThread()
    if (state == newState) {
      return
    }

    // if there are tracked pointers and the gesture is about to end, send event cancelling all pointers
    if (trackedPointersCount > 0 && (newState == STATE_END || newState == STATE_CANCELLED || newState == STATE_FAILED)) {
      cancelPointers()
    }

    val oldState = state
    state = newState
    if (state == STATE_ACTIVE) {
      // Generate a unique coalescing-key each time the gesture-handler becomes active. All events will have
      // the same coalescing-key allowing EventDispatcher to coalesce RNGestureHandlerEvents when events are
      // generated faster than they can be treated by JS thread
      eventCoalescingKey = nextEventCoalescingKey++
    }
    orchestrator!!.onHandlerStateChange(this, newState, oldState)
    onStateChange(newState, oldState)
  }

  fun wantEvents(): Boolean {
    return isEnabled &&
      state != STATE_FAILED &&
      state != STATE_CANCELLED &&
      state != STATE_END &&
      trackedPointersIDsCount > 0
  }

  open fun shouldRequireToWaitForFailure(handler: GestureHandler<*>): Boolean {
    if (handler === this) {
      return false
    }

    return interactionController?.shouldRequireHandlerToWaitForFailure(this, handler) ?: false
  }

  fun shouldWaitForHandlerFailure(handler: GestureHandler<*>): Boolean {
    if (handler === this) {
      return false
    }

    return interactionController?.shouldWaitForHandlerFailure(this, handler) ?: false
  }

  open fun shouldRecognizeSimultaneously(handler: GestureHandler<*>): Boolean {
    if (handler === this) {
      return true
    }

    return interactionController?.shouldRecognizeSimultaneously(this, handler) ?: false
  }

  open fun shouldBeCancelledBy(handler: GestureHandler<*>): Boolean {
    if (handler === this) {
      return false
    }

    return interactionController?.shouldHandlerBeCancelledBy(this, handler) ?: false
  }

  fun isWithinBounds(view: View?, posX: Float, posY: Float): Boolean {
    var left = 0f
    var top = 0f
    var right = view!!.width.toFloat()
    var bottom = view.height.toFloat()
    if (hitSlop != null) {
      val padLeft = hitSlop!![HIT_SLOP_LEFT_IDX]
      val padTop = hitSlop!![HIT_SLOP_TOP_IDX]
      val padRight = hitSlop!![HIT_SLOP_RIGHT_IDX]
      val padBottom = hitSlop!![HIT_SLOP_BOTTOM_IDX]
      if (hitSlopSet(padLeft)) {
        left -= padLeft
      }
      if (hitSlopSet(padTop)) {
        top -= padTop
      }
      if (hitSlopSet(padRight)) {
        right += padRight
      }
      if (hitSlopSet(padBottom)) {
        bottom += padBottom
      }
      val width = hitSlop!![HIT_SLOP_WIDTH_IDX]
      val height = hitSlop!![HIT_SLOP_HEIGHT_IDX]
      if (hitSlopSet(width)) {
        if (!hitSlopSet(padLeft)) {
          left = right - width
        } else if (!hitSlopSet(padRight)) {
          right = left + width
        }
      }
      if (hitSlopSet(height)) {
        if (!hitSlopSet(padTop)) {
          top = bottom - height
        } else if (!hitSlopSet(padBottom)) {
          bottom = top + height
        }
      }
    }
    return posX in left..right && posY in top..bottom
  }

  fun cancel() {
    if (state == STATE_ACTIVE || state == STATE_UNDETERMINED || state == STATE_BEGAN) {
      onCancel()
      moveToState(STATE_CANCELLED)
    }
  }

  fun fail() {
    if (state == STATE_ACTIVE || state == STATE_UNDETERMINED || state == STATE_BEGAN) {
      moveToState(STATE_FAILED)
    }
  }

  fun activate() = activate(force = false)

  open fun activate(force: Boolean) {
    if ((!manualActivation || force) && (state == STATE_UNDETERMINED || state == STATE_BEGAN)) {
      moveToState(STATE_ACTIVE)
    }
  }

  fun begin() {
    if (state == STATE_UNDETERMINED) {
      moveToState(STATE_BEGAN)
    }
  }

  fun end() {
    if (state == STATE_BEGAN || state == STATE_ACTIVE) {
      moveToState(STATE_END)
    }
  }

  protected open fun onHandle(event: MotionEvent) {
    moveToState(STATE_FAILED)
  }

  protected open fun onStateChange(newState: Int, previousState: Int) {}
  protected open fun onReset() {}
  protected open fun onCancel() {}
  fun reset() {
    view = null
    orchestrator = null
    Arrays.fill(trackedPointerIDs, -1)
    trackedPointersIDsCount = 0

    trackedPointersCount = 0
    trackedPointers.fill(null)
    touchEventType = RNGestureHandlerTouchEvent.EVENT_UNDETERMINED
    onReset()
  }

  fun setOnTouchEventListener(listener: OnTouchEventListener?): GestureHandler<*> {
    onTouchEventListener = listener
    return this
  }

  override fun toString(): String {
    val viewString = if (view == null) null else view!!.javaClass.simpleName
    return this.javaClass.simpleName + "@[" + tag + "]:" + viewString
  }

  val lastRelativePositionX: Float
    get() = lastAbsolutePositionX - lastEventOffsetX
  val lastRelativePositionY: Float
    get() = lastAbsolutePositionY - lastEventOffsetY

  companion object {
    const val STATE_UNDETERMINED = 0
    const val STATE_FAILED = 1
    const val STATE_BEGAN = 2
    const val STATE_CANCELLED = 3
    const val STATE_ACTIVE = 4
    const val STATE_END = 5
    const val HIT_SLOP_NONE = Float.NaN
    private const val HIT_SLOP_LEFT_IDX = 0
    private const val HIT_SLOP_TOP_IDX = 1
    private const val HIT_SLOP_RIGHT_IDX = 2
    private const val HIT_SLOP_BOTTOM_IDX = 3
    private const val HIT_SLOP_WIDTH_IDX = 4
    private const val HIT_SLOP_HEIGHT_IDX = 5
    const val DIRECTION_RIGHT = 1
    const val DIRECTION_LEFT = 2
    const val DIRECTION_UP = 4
    const val DIRECTION_DOWN = 8
    private const val MAX_POINTERS_COUNT = 12
    private lateinit var pointerProps: Array<PointerProperties?>
    private lateinit var pointerCoords: Array<PointerCoords?>
    private fun initPointerProps(size: Int) {
      var size = size
      if (!::pointerProps.isInitialized) {
        pointerProps = arrayOfNulls(MAX_POINTERS_COUNT)
        pointerCoords = arrayOfNulls(MAX_POINTERS_COUNT)
      }
      while (size > 0 && pointerProps[size - 1] == null) {
        pointerProps[size - 1] = PointerProperties()
        pointerCoords[size - 1] = PointerCoords()
        size--
      }
    }

    private var nextEventCoalescingKey: Short = 0
    private fun hitSlopSet(value: Float): Boolean {
      return !java.lang.Float.isNaN(value)
    }

    fun stateToString(state: Int): String? {
      when (state) {
        STATE_UNDETERMINED -> return "UNDETERMINED"
        STATE_ACTIVE -> return "ACTIVE"
        STATE_FAILED -> return "FAILED"
        STATE_BEGAN -> return "BEGIN"
        STATE_CANCELLED -> return "CANCELLED"
        STATE_END -> return "END"
      }
      return null
    }
  }

  private data class PointerData(
    val pointerId: Int,
    var x: Float,
    var y: Float,
    var absoluteX: Float,
    var absoluteY: Float
  )
}
