///|
/// A two-finger pinch/rotation sample delivered by `View::on_pinch`.
///
/// The recognizer consumes the multi-pointer transport directly
/// (`PointerEvent.id`): it tracks the first two fingers that press inside the
/// view, and every movement of either finger produces one sample whose deltas
/// are measured since the previous sample — `scale_delta` is the span ratio
/// (1.0 = unchanged, > 1 = fingers apart) and `rotation_delta` is the angle
/// change in radians wrapped to (-π, π]. `focus` is the midpoint of the two
/// tracked pointers.
#derive(Eq, Debug, ToJson)
pub(all) struct PinchGestureEvent {
  phase : GesturePhase
  scale_delta : Double
  rotation_delta : Double
  focus : Point
}

///|
pub extend PinchGestureEvent with Debug::{to_repr}

///|
pub extend PinchGestureEvent with Eq::{not_equal, equal}

///|
pub extend PinchGestureEvent with ToJson::{to_json}

///|
/// Runtime-owned two-finger recognition state. The first two pointer ids
/// that press inside the view are tracked; further fingers are ignored until
/// the gesture ends. `span`/`angle` carry the last emitted geometry so each
/// sample measures the delta since the previous one.
#derive(Eq)
priv struct PinchRecognitionState {
  tracked : Int32
  pointer0_id : Int32
  pointer0 : Point
  pointer1_id : Int32
  pointer1 : Point
  span : Double
  angle : Double
}

///|
impl ViewStateValue for PinchRecognitionState with fn view_state_type_key() {
  DeclarationKey::string("moui.core.PinchRecognitionState.v1")
}

///|
impl ViewStateValue for PinchRecognitionState with fn encode_view_state(self) {
  let output = Buffer(size_hint=60)
  output.write_int_be(self.tracked)
  output.write_int_be(self.pointer0_id)
  output.write_double_be(self.pointer0.x)
  output.write_double_be(self.pointer0.y)
  output.write_int_be(self.pointer1_id)
  output.write_double_be(self.pointer1.x)
  output.write_double_be(self.pointer1.y)
  output.write_double_be(self.span)
  output.write_double_be(self.angle)
  output.to_bytes()
}

///|
impl ViewStateValue for PinchRecognitionState with fn decode_view_state(
  _self,
  bytes,
) {
  if bytes.length() != 60 {
    return None
  }
  let decode_int = fn(offset : Int32) -> Int32? {
    ViewStateValue::decode_view_state(0, bytes[offset:offset + 4].to_owned())
  }
  let decode_double = fn(offset : Int32) -> Double? {
    ViewStateValue::decode_view_state(0.0, bytes[offset:offset + 8].to_owned())
  }
  match
    (
      decode_int(0),
      decode_int(4),
      decode_double(8),
      decode_double(16),
      decode_int(24),
      decode_double(28),
      decode_double(36),
      decode_double(44),
      decode_double(52),
    ) {
    (
      Some(tracked),
      Some(pointer0_id),
      Some(p0x),
      Some(p0y),
      Some(pointer1_id),
      Some(p1x),
      Some(p1y),
      Some(span),
      Some(angle),
    ) =>
      Some({
        tracked,
        pointer0_id,
        pointer0: Point::new(x=p0x, y=p0y),
        pointer1_id,
        pointer1: Point::new(x=p1x, y=p1y),
        span,
        angle,
      })
    _ => None
  }
}

///|
fn pinch_recognition_state_slot() -> ViewStateSlot[PinchRecognitionState] {
  ViewStateSlot::new(
    key=DeclarationKey::string("moui.core.pinch-recognition"),
    initial={
      tracked: 0,
      pointer0_id: 0,
      pointer0: Point::new(x=0.0, y=0.0),
      pointer1_id: 0,
      pointer1: Point::new(x=0.0, y=0.0),
      span: 0.0,
      angle: 0.0,
    },
  )
}

///|
fn pinch_span(a : Point, b : Point) -> Double {
  let dx = b.x - a.x
  let dy = b.y - a.y
  (dx * dx + dy * dy).sqrt()
}

///|
fn pinch_angle(a : Point, b : Point) -> Double {
  @math.atan2(b.y - a.y, b.x - a.x)
}

///|
/// Wrap an angle difference into (-π, π] so a recognizer crossing the ±π
/// seam reports a small delta instead of a full turn.
fn pinch_wrap_angle(delta : Double) -> Double {
  let two_pi = 2.0 * @math.PI
  let mut wrapped = delta % two_pi
  if wrapped > @math.PI {
    wrapped = wrapped - two_pi
  }
  if wrapped <= 0.0 - @math.PI {
    wrapped = wrapped + two_pi
  }
  wrapped
}

///|
fn pinch_midpoint(a : Point, b : Point) -> Point {
  Point::new(x=(a.x + b.x) / 2.0, y=(a.y + b.y) / 2.0)
}

///|
/// One pointer event through the pinch recognizer. Returns the recognition
/// state to store and, when the state machine produces one, the gesture
/// sample to deliver.
fn[Msg] pinch_recognize(
  state : PinchRecognitionState,
  event : PointerEvent,
  inside~ : Bool,
  handler~ : (PinchGestureEvent) -> Msg,
) -> (PinchRecognitionState, Array[Msg]) {
  match event.phase {
    Down =>
      if !inside {
        (state, [])
      } else if state.tracked == 0 {
        (
          {
            ..state,
            tracked: 1,
            pointer0_id: event.id,
            pointer0: event.position,
          },
          [],
        )
      } else if state.tracked == 1 && event.id != state.pointer0_id {
        // The second finger arms the gesture: one Started sample with
        // identity deltas.
        let pointer1 = event.position
        let span = pinch_span(state.pointer0, pointer1)
        let angle = pinch_angle(state.pointer0, pointer1)
        (
          { ..state, tracked: 2, pointer1_id: event.id, pointer1, span, angle, },
          [
            handler({
              phase: Started,
              scale_delta: 1.0,
              rotation_delta: 0.0,
              focus: pinch_midpoint(state.pointer0, pointer1),
            }),
          ],
        )
      } else {
        // A third finger (or a duplicate stream) never joins the gesture.
        (state, [])
      }
    Move =>
      if state.tracked == 2 && event.id == state.pointer0_id {
        let pointer0 = event.position
        let span = pinch_span(pointer0, state.pointer1)
        let angle = pinch_angle(pointer0, state.pointer1)
        let scale_delta = if state.span > 0.0 { span / state.span } else { 1.0 }
        (
          { ..state, pointer0, span, angle, },
          [
            handler({
              phase: Moved,
              scale_delta,
              rotation_delta: pinch_wrap_angle(angle - state.angle),
              focus: pinch_midpoint(pointer0, state.pointer1),
            }),
          ],
        )
      } else if state.tracked == 2 && event.id == state.pointer1_id {
        let pointer1 = event.position
        let span = pinch_span(state.pointer0, pointer1)
        let angle = pinch_angle(state.pointer0, pointer1)
        let scale_delta = if state.span > 0.0 { span / state.span } else { 1.0 }
        (
          { ..state, pointer1, span, angle, },
          [
            handler({
              phase: Moved,
              scale_delta,
              rotation_delta: pinch_wrap_angle(angle - state.angle),
              focus: pinch_midpoint(state.pointer0, pointer1),
            }),
          ],
        )
      } else {
        (state, [])
      }
    Up =>
      if state.tracked == 2 &&
        (event.id == state.pointer0_id || event.id == state.pointer1_id) {
        // A finger lift ends the gesture with identity deltas focused on the
        // last midpoint; the state clears so the next pair starts fresh.
        (
          {
            tracked: 0,
            pointer0_id: 0,
            pointer0: Point::new(x=0.0, y=0.0),
            pointer1_id: 0,
            pointer1: Point::new(x=0.0, y=0.0),
            span: 0.0,
            angle: 0.0,
          },
          [
            handler({
              phase: Ended,
              scale_delta: 1.0,
              rotation_delta: 0.0,
              focus: pinch_midpoint(state.pointer0, state.pointer1),
            }),
          ],
        )
      } else if state.tracked == 1 && event.id == state.pointer0_id {
        // The lone finger lifted before a second ever joined: nothing armed.
        (
          {
            tracked: 0,
            pointer0_id: 0,
            pointer0: Point::new(x=0.0, y=0.0),
            pointer1_id: 0,
            pointer1: Point::new(x=0.0, y=0.0),
            span: 0.0,
            angle: 0.0,
          },
          [],
        )
      } else {
        (state, [])
      }
    // An explicit cancel or hover exit tears the recognition down silently:
    // no sample, no arithmetic, the next pair starts fresh.
    Cancel | Exit =>
      (
        {
          tracked: 0,
          pointer0_id: 0,
          pointer0: Point::new(x=0.0, y=0.0),
          pointer1_id: 0,
          pointer1: Point::new(x=0.0, y=0.0),
          span: 0.0,
          angle: 0.0,
        },
        [],
      )
  }
}

///|
priv struct OnPinchModifier[Msg] {
  handler : (PinchGestureEvent) -> Msg
}

///|
fn[Msg] OnPinchModifier::to_any_modifier(
  self : OnPinchModifier[Msg],
) -> AnyModifier[Msg] {
  let handler = self.handler
  {
    ..noop_modifier(),
    identity_kind: "OnPinch",
    event: fn(ctx) {
      match ctx.event {
        Pointer(event) if event.button == 0 => {
          let slot = pinch_recognition_state_slot()
          let state = ctx.state.read_slot(slot)
          let (next, samples) = pinch_recognize(
            state,
            event,
            inside=ctx.frame.contains(event.position),
            handler~,
          )
          let messages = samples
          if messages.is_empty() {
            // State-only changes (arming, clearing) still commit: the arena
            // arms candidates from slots_changed, and terminal cleanup must
            // not leave a stale pair behind.
            let changed = next != state
            {
              ..ViewEventResult::ignored(),
              changed,
              captured: state.tracked > 0 && next.tracked > 0,
              state: Some(
                if next.tracked == 0 && state.tracked > 0 {
                  ctx.state.without_slot(slot)
                } else {
                  ctx.state.with_slot(slot, next)
                },
              ),
              dirty: if changed {
                ViewPaintDirty
              } else {
                ViewClean
              },
            }
          } else {
            {
              ..ViewEventResult::ignored(),
              changed: true,
              activated: true,
              captured: true,
              state: Some(ctx.state.with_slot(slot, next)),
              dirty: ViewPaintDirty,
              messages,
            }
          }
        }
        _ => ViewEventResult::ignored()
      }
    },
  }
}