// SPDX-License-Identifier: Apache-2.0
//
// OpenCVStitcher.h
//
// Objective-C interface to the OpenCV stitcher.  All C++ types
// (`cv::Stitcher`, `cv::Mat`, `std::vector`) are confined to the
// implementation file (`.mm`) so this header can be imported from
// pure Swift without dragging in the C++ standard library.
//
// Why the layered design (ObjC interface ↔ ObjC++ impl ↔ C++ lib)?
//   The Swift importer does not understand C++.  Without this layer
//   we'd have to write a much heavier @objc shim using opaque void*
//   pointers.  Letting Objective-C own the boundary types gives us
//   automatic memory management for NSString/NSError/NSDictionary
//   and zero copy on the boundary — the .mm only does the C++→C++
//   work, never marshalling.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/// NSError domain raised by OpenCVStitcher errors.  Codes match the
/// `cv::Stitcher::Status` enum values so callers can branch on
/// "needs more images" vs. "homography failed".
extern NSString *const RNImageStitcherErrorDomain;

/// Result of a successful stitch — pixel dimensions of the panorama
/// plus the path it was written to (host app passed it in).
@interface RNStitchResult : NSObject
@property (nonatomic, copy, readonly) NSString *outputPath;
@property (nonatomic, assign, readonly) NSInteger width;
@property (nonatomic, assign, readonly) NSInteger height;
@property (nonatomic, assign, readonly) double durationMs;
/// 2026-05-16 (Issue 5) — stitch-retry telemetry sourced from
/// `retailens::StitchResult`.  Surface in the JS finalize dict so the
/// host can render a debug toast on retry.  Since 2026-08-17 the values
/// come from the flattened 4-rung ladder's WINNING rung (the legacy
/// multi-attempt C+D loop survives only on the manual opt-in path).
///
///   framesRequested:        number of keyframes handed to the stitcher
///   framesIncluded:         number retained after leaveBiggestComponent
///   finalConfidenceThresh:  threshold the winning rung/attempt used
///                            (pan 1.0/0.3, scans 1.0/0.5); -1.0 when
///                            no retry data exists (rare error paths).
///                            NOT an escalation/attempt-count signal:
///                            a 1.0 can be a later rung's win after
///                            earlier rungs failed (flattened ladder)
@property (nonatomic, assign, readonly) NSInteger framesRequested;
@property (nonatomic, assign, readonly) NSInteger framesIncluded;
@property (nonatomic, assign, readonly) double finalConfidenceThresh;
/// 2026-06-14 (DEV overlay) — semicolon-separated `key=value` trace of the
/// stitcher's runtime choices for this output (pipeline/warper/route/seam/
/// blend), surfaced on the preview in __DEV__.  Empty string when unavailable.
@property (nonatomic, copy, readonly) NSString *debugSummary;
- (instancetype)initWithOutputPath:(NSString *)outputPath
                             width:(NSInteger)width
                            height:(NSInteger)height
                        durationMs:(double)durationMs
                   framesRequested:(NSInteger)framesRequested
                    framesIncluded:(NSInteger)framesIncluded
             finalConfidenceThresh:(double)finalConfidenceThresh NS_DESIGNATED_INITIALIZER;
/// Convenience initializer for paths that don't carry C+D retry
/// telemetry (e.g. stitchVideoAtPath / stitchFramePaths).  Sets
/// the telemetry fields to sentinel values (-1) so JS callers can
/// detect "no retry data available" cleanly.
- (instancetype)initWithOutputPath:(NSString *)outputPath
                             width:(NSInteger)width
                            height:(NSInteger)height
                        durationMs:(double)durationMs;
- (instancetype)init NS_UNAVAILABLE;
@end


@interface OpenCVStitcher : NSObject

/// Stitch the images at `framePaths` into a single panoramic JPEG
/// at `outputPath`.
///
/// `quality`: JPEG quality [0..100].  Caller-clamped; 0 / 101 will
/// be coerced into range by the impl.
///
/// On success returns the result object; on failure populates
/// `error` (NSError, RNImageStitcherErrorDomain) and returns nil.
/// `warperType`: one of @"plane" / @"cylindrical" / @"spherical".
///   Pass nil/empty for the default (@"plane").  Different
///   projections suit different gestures — see the field A/B
///   testing settings UI for guidance.
/// `blenderType`: one of @"multiband" / @"feather".  Pass nil for
///   the default (@"multiband").
/// `seamFinderType`: one of @"graphcut" / @"skip".  Pass nil for
///   the default (@"graphcut").
///   - "graphcut" runs cv::detail::GraphCutSeamFinder over all
///     warped frames before blending — produces clean seams,
///     pairs well with MultiBandBlender, but holds all warped
///     frames in memory simultaneously (higher peak).
///   - "skip" streams warp+feed in a single pass and never holds
///     more than one warped frame.  Lower peak memory.  Use on
///     low-RAM devices or for fastest path with FeatherBlender.
/// AR-STITCHING-TWO-MODES — see memory/ar-stitching-two-modes.md
///
///   - `captureOrientation` ("portrait" | "portrait-upside-down" |
///     "landscape-left" | "landscape-right"): physical phone hold at
///     capture start, sourced from the JS-side accelerometer hook.
///     Drives the OUTPUT panorama's bake-rotation per the two
///     supported capture modes:
///       portrait              → no bake-rotation
///       portrait-upside-down  → bake ROTATE_180
///       landscape-left        → bake ROTATE_90_COUNTERCLOCKWISE
///       landscape-right       → bake ROTATE_90_CLOCKWISE
///     Output JPEG is always written with EXIF=1 (no metadata
///     rotation) since the rotation is baked into the pixels.
///
///   - **Maximum-inscribed-rectangle crop** instead of bounding-
///     rectangle.  cv::Stitcher's output has irregular black corners
///     where the projection didn't fill; bbox crop still included
///     them.  With `useInscribedRectCrop:YES` we find the largest
///     axis-aligned rectangle entirely inside the non-zero region
///     and crop to that — clean output with no black corners.
/// `useManualPipeline`: YES → the legacy manual cv::detail pipeline;
///   NO → stock high-level cv::Stitcher.  Since 2026-06-16 every production
///   caller passes NO ("high level across the board" — batch finalize,
///   refine, both platforms); YES is an explicit opt-in nothing ships with.
+ (nullable RNStitchResult *)stitchFramePaths:(NSArray<NSString *> *)framePaths
                                          outputPath:(NSString *)outputPath
                                         jpegQuality:(NSInteger)quality
                                          warperType:(nullable NSString *)warperType
                                         blenderType:(nullable NSString *)blenderType
                                      seamFinderType:(nullable NSString *)seamFinderType
                                  captureOrientation:(nullable NSString *)captureOrientation
                                useInscribedRectCrop:(BOOL)useInscribedRectCrop
                                          stitchMode:(nullable NSString *)stitchMode
                                   useManualPipeline:(BOOL)useManualPipeline
                                               error:(NSError **)error;

/// Overload adding explicit staged-resolution budgets (StitchOptions
/// passthroughs).
///
/// `compositingResolMP` > 0 overrides the wrapper's 1.0 MP per-frame compose
/// pin (megapixels; the shared-C++ canvas-budget guard still downscales when
/// the total canvas exceeds the RAM budget, so a large value stays
/// memory-safe).  <= 0 keeps the historical 1.0 MP.
///
/// `registrationResolMP` > 0 overrides the 0.6 MP registration budget
/// (cv::Stitcher's own default) the wrapper pins.  <= 0 keeps the historical
/// 0.6 MP.
///
/// The legacy selector above delegates here with -1/-1, so existing callers
/// are behaviour-identical.
+ (nullable RNStitchResult *)stitchFramePaths:(NSArray<NSString *> *)framePaths
                                          outputPath:(NSString *)outputPath
                                         jpegQuality:(NSInteger)quality
                                          warperType:(nullable NSString *)warperType
                                         blenderType:(nullable NSString *)blenderType
                                      seamFinderType:(nullable NSString *)seamFinderType
                                  captureOrientation:(nullable NSString *)captureOrientation
                                useInscribedRectCrop:(BOOL)useInscribedRectCrop
                                          stitchMode:(nullable NSString *)stitchMode
                                   useManualPipeline:(BOOL)useManualPipeline
                                  compositingResolMP:(double)compositingResolMP
                                 registrationResolMP:(double)registrationResolMP
                                               error:(NSError **)error;

/// Extract `maxFrames` evenly-spaced frames from the video at
/// `videoPath`, write each as a JPEG into `outputDir`, return the
/// list of file paths in capture order.
///
/// Used as the first half of the panorama pipeline: the host app
/// records video while the user holds the shutter, then we sample
/// it down to N still frames the stitcher can consume.  cv::Stitcher
/// works best with 5-15 well-spaced frames — much more is redundant
/// and slow; much less risks gaps in the seam.
///
/// Implementation uses AVAssetImageGenerator (Foundation), not
/// OpenCV, so no C++ touches this path; cheap enough that we can
/// expose it as a separate primitive too.
+ (nullable NSArray<NSString *> *)extractFramesFromVideoAtPath:(NSString *)videoPath
                                                     outputDir:(NSString *)outputDir
                                                     maxFrames:(NSInteger)maxFrames
                                                   jpegQuality:(NSInteger)quality
                                                         error:(NSError **)error;

/// One-shot helper: extract frames from `videoPath`, stitch them
/// into a panorama at `outputPath`, delete the temporary frames,
/// return the result.  This is what the JS shutter-hold flow calls;
/// callers don't have to manage their own tmp directory or clean
/// up partial state on failure.
+ (nullable RNStitchResult *)stitchVideoAtPath:(NSString *)videoPath
                                           outputPath:(NSString *)outputPath
                                            maxFrames:(NSInteger)maxFrames
                                          jpegQuality:(NSInteger)quality
                                           warperType:(nullable NSString *)warperType
                                          blenderType:(nullable NSString *)blenderType
                                       seamFinderType:(nullable NSString *)seamFinderType
                                                error:(NSError **)error;

/// Normalise the EXIF orientation of `imagePath` in place.
///
/// vision-camera writes photos with the camera-sensor's native
/// landscape pixels and an EXIF Orientation tag describing how to
/// rotate them for display.  Most consumers (iOS UIImage, RN's
/// <Image>) honour the tag, but Sentry breadcrumbs, share sheets,
/// downstream image-manipulation libs, and the cv::Stitcher all
/// read raw pixels and end up sideways.
///
/// This method round-trips the file through cv::imread (which
/// honours EXIF and gives us the post-rotation pixel buffer) and
/// cv::imwrite (which writes a plain JPEG with NO EXIF), so the
/// saved file ends up with rotation baked into pixels and no
/// orientation metadata.  Idempotent on already-normalised images.
///
/// Returns `@{ @"width": NSNumber, @"height": NSNumber }` post
/// rotation so the caller can update its CaptureResult dimensions
/// to match what's now on disk.  NSDictionary is used (rather than
/// CGSize) because Swift can't translate `(CGSize) + (NSError**)`
/// into a throwing API — a nullable reference type is required.
+ (nullable NSDictionary<NSString *, NSNumber *> *)normaliseImageAtPath:(NSString *)imagePath
                                                                  error:(NSError **)error;

/// v0.15 debug — compute the max-inscribed rectangle of the non-black
/// region of the JPEG at `imagePath` WITHOUT modifying the file.
/// Returns `{ x, y, width, height, imageWidth, imageHeight }` so the JS
/// debug harness can overlay the rect on the full image.  Reuses the
/// same `MaxInscribedRectFromMask` the production crop uses.
+ (nullable NSDictionary<NSString *, NSNumber *> *)computeInscribedRectAtPath:(NSString *)imagePath
                                                                       error:(NSError **)error;

/// v0.15 debug — crop the JPEG at `imagePath` to the given rectangle
/// (clamped to image bounds), re-encode at `quality`, overwrite in
/// place.  Returns the final `{ width, height }`.
+ (nullable NSDictionary<NSString *, NSNumber *> *)cropToRectAtPath:(NSString *)imagePath
                                                                 x:(NSInteger)x
                                                                 y:(NSInteger)y
                                                             width:(NSInteger)width
                                                            height:(NSInteger)height
                                                           quality:(NSInteger)quality
                                                             error:(NSError **)error;

/// item-7 — free-quad perspective crop.  Takes 4 IMAGE-PIXEL corners
/// (ordered TL, TR, BR, BL) and rectifies the enclosed quadrilateral to
/// an upright rectangle (cv::getPerspectiveTransform + warpPerspective),
/// re-encodes at `quality`, overwrites in place.  Returns the rectified
/// `{ width, height }`.  Rejects a degenerate / non-convex / out-of-bounds
/// quad, and guards the output canvas with the shared canvasExceedsGuard.
+ (nullable NSDictionary<NSString *, NSNumber *> *)cropToQuadAtPath:(NSString *)imagePath
                                                               tlX:(double)tlX
                                                               tlY:(double)tlY
                                                               trX:(double)trX
                                                               trY:(double)trY
                                                               brX:(double)brX
                                                               brY:(double)brY
                                                               blX:(double)blX
                                                               blY:(double)blY
                                                           quality:(NSInteger)quality
                                                             error:(NSError **)error;

/// v0.15 debug — write a red-tinted overlay JPEG (excluded / sub-threshold
/// pixels rendered red) next to `imagePath` (suffix ".mask.jpg") so the
/// harness can show WHY the inscribed rect lands where it does. Returns
/// `{ maskPath, width, height, excludedPercent }`.
+ (nullable NSDictionary *)debugMaskOverlayAtPath:(NSString *)imagePath
                                        threshold:(NSInteger)threshold
                                            error:(NSError **)error;

@end

NS_ASSUME_NONNULL_END
