// SPDX-License-Identifier: Apache-2.0
//
// KeyframeGateBridge.h — Obj-C++ wrapper exposing the shared C++
// KeyframeGate (in react-native-image-stitcher/cpp/) to Swift.
//
// Why this exists:
//   The pose-driven keyframe-selection algorithm is the single most
//   important quality-determining piece of the panorama pipeline.
//   Historically it lived in pure Swift (KeyframeGate.swift), which
//   meant the Android side had to either re-implement it (parity
//   risk — confirmed bug in the V16 frame-counter MVP placeholder)
//   or skip it.  We've now ported the algorithm to shared C++ in
//   cpp/keyframe_gate.{hpp,cpp}; this Obj-C++ bridge is the thin
//   shim that lets Swift call into the same C++ code that the JNI
//   side will call on Android.
//
// Threading:
//   The C++ KeyframeGate is NOT thread-safe.  Caller (Swift) must
//   serialise — typically via the engine's workQueue.  Same
//   contract as the Swift-only KeyframeGate had before.

#import <Foundation/Foundation.h>
#import <CoreVideo/CVPixelBuffer.h>

NS_ASSUME_NONNULL_BEGIN

/// Mirror of `retailens::GateStrategy` (keyframe_gate.hpp).  Bridged as
/// raw NSInteger across Obj-C; the Swift facade lifts it to an enum.
/// MUST stay 1:1 with the C++ enum integer values.
typedef NS_ENUM(NSInteger, KGBStrategy) {
    KGBStrategyPose = 0,   ///< Plane-projection-overlap path (default)
    KGBStrategyFlow = 1,   ///< Sparse-optical-flow novelty (V16 A2)
};

/// Mirror of `retailens::KeyframeGateDecision` in keyframe_gate.hpp.
/// `reasonCode` is the raw int32 of the C++ enum; `reasonString` is
/// the human-readable label matching the original Swift telemetry
/// strings (so JS telemetry stays bit-identical).
NS_SWIFT_NAME(KeyframeGateBridgeDecision)
@interface KGBDecision : NSObject
@property (nonatomic, readonly) BOOL      accept;
@property (nonatomic, readonly) NSInteger reasonCode;
@property (nonatomic, readonly) NSString *reasonString;
@property (nonatomic, readonly) double    newContentFraction;
@property (nonatomic, readonly) NSInteger acceptedCount;
@property (nonatomic, readonly) NSInteger maxCount;
@end

/// Thin Obj-C++ wrapper around `retailens::KeyframeGate`.  All
/// methods are 1:1 with the C++ API except `evaluate…`, which
/// flattens the Swift call shape (pose struct + optional plane
/// matrix) into primitive C-callable args.
NS_SWIFT_NAME(KeyframeGateBridge)
@interface KeyframeGateBridge : NSObject

- (instancetype)init;

// ── Settings ────────────────────────────────────────────────────
- (void)setEnabled:(BOOL)enabled;
- (void)setOverlapThreshold:(double)threshold;
- (void)setMaxCount:(NSInteger)maxCount;
- (void)markNextFrameAsLast;
- (void)reset;

// ── Strategy + Flow params (V16 A2) ──────────────────────────────
- (void)setStrategy:(KGBStrategy)strategy;
- (KGBStrategy)strategy;
- (void)setFlowMaxCorners:(NSInteger)maxCorners;
- (void)setFlowQualityLevel:(double)quality;
- (void)setFlowMinDistance:(double)minDistance;
/// V16 — translation budget (metres).  Set > 0 to force-accept on
/// translation overflow even when novelty < threshold; 0 disables.
/// See KeyframeGate.swift for the operator-facing description.
- (void)setFlowMaxTranslationM:(double)metres;
/// Wall-clock keyframe-interval budget (milliseconds).  Set > 0 to
/// force-accept a frame when the elapsed time since the last accepted
/// keyframe exceeds this value (applies to BOTH Pose and Flow
/// strategies); 0 disables.  Passed straight through (no unit
/// conversion).  See KeyframeGate.swift for the operator-facing
/// description.
- (void)setMaxKeyframeIntervalMs:(double)ms;
/// V16 — novelty aggregation percentile [0.5, 0.99].  Default 0.85.
/// See KeyframeGate.swift for the operator-facing description.
- (void)setFlowNoveltyPercentile:(double)percentile;

/// 2026-05-22 (audit F1b) — non-AR-mode opt-out for the angular-
/// delta fallback path.  In non-AR captures there is no
/// ARKit/ARCore pose, so the gate's angular-delta computation runs
/// on gyro-integrated yaw/pitch which drifts ~1–2°/min.  Drift
/// accumulates past the overlap threshold even when the camera
/// hasn't moved → near-identical frames get accepted → cv::Stitcher
/// camera-param estimator goes degenerate → "warpRoi too large"
/// crash on finalize.  Set this to `true` in non-AR mode to disable
/// the angular-delta fallback entirely (the Flow strategy still
/// works when pixel data is supplied).  Default `false`
/// (back-compat — AR mode uses the fallback).
- (void)setDisableAngularFallback:(BOOL)disabled;

/// v0.25 — per-frame AR tracking trust.  Pass `NO` while ARKit reports
/// anything other than `.normal` tracking: an initialising /
/// relocalising world transform slides and snaps by metres/radians
/// between consecutive frames, and the gate's two POSE-DRIVEN
/// force-accepts (translation budget + angular fallback) would fire on
/// a camera that never moved — burst-accepting to the keyframe cap and
/// auto-finalising the operator's hold (measured at ~415 ms in the
/// v0.24.x field RCA).  With `NO` the gate falls back to exactly the
/// non-AR configuration: image novelty + the wall-clock time budget.
/// Default `YES` (back-compat).
- (void)setPoseTrusted:(BOOL)trusted;

/// v0.25 — may a keep-alive (time-budget) accept be the accept that
/// REACHES maxCount and therefore ends the capture via the host's
/// count-based auto-finalize?  Default YES = pre-0.25 behaviour.
/// Set NO so a stationary hold cannot self-finalize on the clock.
- (void)setTimeIntervalCanFinalize:(BOOL)canFinalize;

// ── Read-only state ─────────────────────────────────────────────
- (BOOL)isEnabled;
- (NSInteger)acceptedCount;
- (NSInteger)maxCount;

/// Evaluate one frame (pose-only).  Pass `plane16` = nil to trigger
/// the C++ angular-delta fallback; otherwise pass a 16-element NSArray
/// of NSNumber (NSDoubles or NSFloats) holding the plane transform
/// column-major (matching `simd_float4x4` element order).
///
/// Backward-compat entry point — always runs the C++ Pose strategy
/// regardless of `strategy` setting (since Flow needs the frame).
/// New code should call `evaluatePixelBuffer:…` below.
- (KGBDecision *)evaluateWithTx:(float)tx
                              ty:(float)ty
                              tz:(float)tz
                              qx:(float)qx
                              qy:(float)qy
                              qz:(float)qz
                              qw:(float)qw
                              fx:(float)fx
                              fy:(float)fy
                              cx:(float)cx
                              cy:(float)cy
                       imageWidth:(int32_t)imageWidth
                      imageHeight:(int32_t)imageHeight
                          plane16:(nullable NSArray<NSNumber *> *)plane16;

/// V16 A2 — strategy-aware evaluate that also accepts the frame's
/// pixel buffer.  Required by Flow strategy (sparse-optical-flow
/// novelty needs the image content).  Pose strategy ignores the
/// pixel buffer here — same result + cost as `evaluateWith…plane16:`.
///
/// Supported pixel formats:
///   * `kCVPixelFormatType_420YpCbCr8BiPlanar{FullRange,VideoRange}`
///     — ARKit's native format.  Y plane is read directly as
///     grayscale (no conversion cost).
///   * `kCVPixelFormatType_32BGRA` — converted to grayscale via
///     `cv::cvtColor` (~2-3 ms at 1920×1440).
/// Other formats → falls through to Pose strategy (defensive).
///
/// The buffer is locked for the duration of the call.  Caller can
/// safely release/recycle the buffer after this method returns.
- (KGBDecision *)evaluatePixelBuffer:(CVPixelBufferRef)pixelBuffer
                                  tx:(float)tx
                                  ty:(float)ty
                                  tz:(float)tz
                                  qx:(float)qx
                                  qy:(float)qy
                                  qz:(float)qz
                                  qw:(float)qw
                                  fx:(float)fx
                                  fy:(float)fy
                                  cx:(float)cx
                                  cy:(float)cy
                          imageWidth:(int32_t)imageWidth
                         imageHeight:(int32_t)imageHeight
                             plane16:(nullable NSArray<NSNumber *> *)plane16
    NS_SWIFT_NAME(evaluate(pixelBuffer:tx:ty:tz:qx:qy:qz:qw:fx:fy:cx:cy:imageWidth:imageHeight:plane16:));

@end

NS_ASSUME_NONNULL_END
