#ifdef RCT_NEW_ARCH_ENABLED

#import "CKCameraViewComponentView.h"

#import <React/RCTBridge+Private.h>
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <folly/dynamic.h>

#import <react/renderer/components/NativeCameraKitSpec/ComponentDescriptors.h>
#import <react/renderer/components/NativeCameraKitSpec/EventEmitters.h>
#import <react/renderer/components/NativeCameraKitSpec/Props.h>
#import <react/renderer/components/NativeCameraKitSpec/RCTComponentViewHelpers.h>

#import "ReactNativeCameraKit-Swift.pre.h"

using namespace facebook::react;

static id CKConvertFollyDynamicToId(const folly::dynamic &dyn) {
  // I could imagine an implementation which avoids copies by wrapping the
  // dynamic in a derived class of NSDictionary.  We can do that if profiling
  // implies it will help.

  switch (dyn.type()) {
  case folly::dynamic::NULLT:
    return nil;
  case folly::dynamic::BOOL:
    return dyn.getBool() ? @YES : @NO;
  case folly::dynamic::INT64:
    return @(dyn.getInt());
  case folly::dynamic::DOUBLE:
    return @(dyn.getDouble());
  case folly::dynamic::STRING:
    return [[NSString alloc] initWithBytes:dyn.c_str()
                                    length:dyn.size()
                                  encoding:NSUTF8StringEncoding];
  case folly::dynamic::ARRAY: {
    NSMutableArray *array =
        [[NSMutableArray alloc] initWithCapacity:dyn.size()];
    for (const auto &elem : dyn) {
      id value = CKConvertFollyDynamicToId(elem);
      if (value) {
        [array addObject:value];
      }
    }
    return array;
  }
  case folly::dynamic::OBJECT: {
    NSMutableDictionary *dict =
        [[NSMutableDictionary alloc] initWithCapacity:dyn.size()];
    for (const auto &elem : dyn.items()) {
      id key = CKConvertFollyDynamicToId(elem.first);
      id value = CKConvertFollyDynamicToId(elem.second);
      if (key && value) {
        dict[key] = value;
      }
    }
    return dict;
  }
  }
}

@interface CKCameraViewComponentView () <RCTCKCameraViewProtocol>
@end

@implementation CKCameraViewComponentView {
  CKCameraView *_view;
}

// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load {
  [super load];
}

- (instancetype)initWithFrame:(CGRect)frame {
  if (self = [super initWithFrame:frame]) {
    static const auto defaultProps = std::make_shared<const CKCameraProps>();
    _props = defaultProps;
    [self prepareView];
  }

  return self;
}

- (void)prepareView {
  _view = [[CKCameraView alloc] init];

  // just need to pass something, it won't really be used on fabric, but it's
  // used to create events (it won't impact sending them)
  _view.reactTag = @-1;

  __weak __typeof__(self) weakSelf = self;

  [_view setOnReadCode:^(NSDictionary *event) {
    __typeof__(self) strongSelf = weakSelf;

    if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
      std::string codeStringValue =
          [event valueForKey:@"codeStringValue"] == nil
              ? ""
              : std::string(
                    [[event valueForKey:@"codeStringValue"] UTF8String]);
      std::string codeFormat =
          [event valueForKey:@"codeFormat"] == nil
              ? ""
              : std::string([[event valueForKey:@"codeFormat"] UTF8String]);
      std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
          strongSelf->_eventEmitter)
          ->onReadCode(
              {.codeStringValue = codeStringValue, .codeFormat = codeFormat});
    }
  }];
  [_view setOnOrientationChange:^(NSDictionary *event) {
    __typeof__(self) strongSelf = weakSelf;

    if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
      id orientation = [event valueForKey:@"orientation"] == nil
                           ? 0
                           : [event valueForKey:@"orientation"];
      std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
          strongSelf->_eventEmitter)
          ->onOrientationChange({.orientation = [orientation intValue]});
    }
  }];
  [_view setOnZoom:^(NSDictionary *event) {
    __typeof__(self) strongSelf = weakSelf;

    if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
      id zoom =
          [event valueForKey:@"zoom"] == nil ? 0 : [event valueForKey:@"zoom"];
      std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
          strongSelf->_eventEmitter)
          ->onZoom({.zoom = [zoom doubleValue]});
    }
  }];
  [_view setOnCaptureButtonPressIn:^(NSDictionary *event) {
    __typeof__(self) strongSelf = weakSelf;

    if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
      std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
          strongSelf->_eventEmitter)
          ->onCaptureButtonPressIn({});
    }
  }];
  [_view setOnCaptureButtonPressOut:^(NSDictionary *event) {
    __typeof__(self) strongSelf = weakSelf;

    if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
      std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
          strongSelf->_eventEmitter)
          ->onCaptureButtonPressOut({});
    }
  }];

  self.contentView = _view;
}

#pragma mark - RCTComponentViewProtocol

+ (ComponentDescriptorProvider)componentDescriptorProvider {
  return concreteComponentDescriptorProvider<CKCameraComponentDescriptor>();
}

- (void)updateLayoutMetrics:
            (const facebook::react::LayoutMetrics &)layoutMetrics
           oldLayoutMetrics:
               (const facebook::react::LayoutMetrics &)oldLayoutMetrics {
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
  [_view updateSubviewsBounds:RCTCGRectFromRect(layoutMetrics.frame)];
}

- (void)updateProps:(const Props::Shared &)props
           oldProps:(const Props::Shared &)oldProps {
  const auto &oldViewProps =
      *std::static_pointer_cast<CKCameraProps const>(_props);
  const auto &newProps = *std::static_pointer_cast<CKCameraProps const>(props);

  NSMutableArray<NSString *> *changedProps = [NSMutableArray new];

  // Keep changedProps aligned with CameraView.swift didSetProps
  // Include event-related props so CameraView can update listeners/state
  [changedProps addObject:@"onOrientationChange"];
  [changedProps addObject:@"onZoom"];
  [changedProps addObject:@"onReadCode"];

  if (oldViewProps.cameraType != newProps.cameraType) {
    _view.cameraType =
        newProps.cameraType == "back" ? CKCameraTypeBack : CKCameraTypeFront;
    [changedProps addObject:@"cameraType"];
  }
  if (oldViewProps.resizeMode != newProps.resizeMode) {
    _view.resizeMode = newProps.resizeMode == "contain" ? CKResizeModeContain
                                                        : CKResizeModeCover;
    [changedProps addObject:@"resizeMode"];
  }
  id flashMode = CKConvertFollyDynamicToId(newProps.flashMode);
  if (oldViewProps.flashMode != newProps.flashMode) {
    _view.flashMode = [flashMode isEqualToString:@"auto"] ? CKFlashModeAuto
                      : [flashMode isEqualToString:@"on"] ? CKFlashModeOn
                                                          : CKFlashModeOff;
    [changedProps addObject:@"flashMode"];
  }
  if (oldViewProps.maxPhotoQualityPrioritization !=
      newProps.maxPhotoQualityPrioritization) {
    if (newProps.maxPhotoQualityPrioritization == "balanced") {
      _view.maxPhotoQualityPrioritization =
          CKMaxPhotoQualityPrioritizationBalanced;
    } else if (newProps.maxPhotoQualityPrioritization == "quality") {
      _view.maxPhotoQualityPrioritization =
          CKMaxPhotoQualityPrioritizationQuality;
    } else {
      _view.maxPhotoQualityPrioritization =
          CKMaxPhotoQualityPrioritizationSpeed;
    }
    [changedProps addObject:@"maxPhotoQualityPrioritization"];
  }
  if (oldViewProps.torchMode != newProps.torchMode) {
    _view.torchMode =
        newProps.torchMode == "on" ? CKTorchModeOn : CKTorchModeOff;
    [changedProps addObject:@"torchMode"];
  }
  id ratioOverlay = CKConvertFollyDynamicToId(newProps.ratioOverlay);
  if (ratioOverlay != nil) {
    _view.ratioOverlay = ratioOverlay;
    [changedProps addObject:@"ratioOverlay"];
  }
  if (oldViewProps.ratioOverlayColor != newProps.ratioOverlayColor) {
    _view.ratioOverlayColor =
        RCTUIColorFromSharedColor(newProps.ratioOverlayColor);
    [changedProps addObject:@"ratioOverlayColor"];
  }
  if (_view.scanBarcode != newProps.scanBarcode) {
    _view.scanBarcode = newProps.scanBarcode;
    [changedProps addObject:@"scanBarcode"];
  }
  if (_view.showFrame != newProps.showFrame) {
    _view.showFrame = newProps.showFrame;
    [changedProps addObject:@"showFrame"];
  }
  if (newProps.scanThrottleDelay > -1) {
    _view.scanThrottleDelay = newProps.scanThrottleDelay;
    [changedProps addObject:@"scanThrottleDelay"];
  }
  if (oldViewProps.frameColor != newProps.frameColor) {
    _view.frameColor = RCTUIColorFromSharedColor(newProps.frameColor);
    [changedProps addObject:@"frameColor"];
  }
  if (oldViewProps.laserColor != newProps.laserColor) {
    UIColor *laserColor = RCTUIColorFromSharedColor(newProps.laserColor);
    _view.laserColor = laserColor;
    [changedProps addObject:@"laserColor"];
  }
  if (oldViewProps.resetFocusTimeout != newProps.resetFocusTimeout) {
    _view.resetFocusTimeout = newProps.resetFocusTimeout;
    [changedProps addObject:@"resetFocusTimeout"];
  }
  if (_view.resetFocusWhenMotionDetected !=
      newProps.resetFocusWhenMotionDetected) {
    _view.resetFocusWhenMotionDetected = newProps.resetFocusWhenMotionDetected;
    [changedProps addObject:@"resetFocusWhenMotionDetected"];
  }
  if (oldViewProps.focusMode != newProps.focusMode) {
    id focusMode = CKConvertFollyDynamicToId(newProps.focusMode);
    _view.focusMode =
        [focusMode isEqualToString:@"on"] ? CKFocusModeOn : CKFocusModeOff;
    [changedProps addObject:@"focusMode"];
  }
  if (oldViewProps.zoomMode != newProps.zoomMode) {
    id zoomMode = CKConvertFollyDynamicToId(newProps.zoomMode);
    _view.zoomMode =
        [zoomMode isEqualToString:@"on"] ? CKZoomModeOn : CKZoomModeOff;
    [changedProps addObject:@"zoomMode"];
  }
  if (oldViewProps.zoom != newProps.zoom) {
    _view.zoom = newProps.zoom > -1 ? @(newProps.zoom) : nil;
    [changedProps addObject:@"zoom"];
  }
  if (oldViewProps.maxZoom != newProps.maxZoom) {
    _view.maxZoom = newProps.maxZoom > -1 ? @(newProps.maxZoom) : nil;
    [changedProps addObject:@"maxZoom"];
  }
  if (oldViewProps.iOsDeferredStart != newProps.iOsDeferredStart) {
    _view.iOsDeferredStart = newProps.iOsDeferredStart;
    [changedProps addObject:@"iOsDeferredStart"];
  }
  float barcodeWidth = newProps.barcodeFrameSize.width;
  float barcodeHeight = newProps.barcodeFrameSize.height;
  if (barcodeWidth != [_view.barcodeFrameSize[@"width"] floatValue] ||
      barcodeHeight != [_view.barcodeFrameSize[@"height"] floatValue]) {
    _view.barcodeFrameSize =
        @{@"width" : @(barcodeWidth), @"height" : @(barcodeHeight)};
    [changedProps addObject:@"barcodeFrameSize"];
  }
  // Since viewprops optional props isn't supported in all RN versions,
  // we assume empty arrays mean it's not defined / ignore changes to it.
  // if the user/dev wants to NOT define the prop, they can simply use
  // scanBarcode={false}
  if (!newProps.allowedBarcodeTypes.empty()) {
    folly::dynamic allowedBarcodeTypesDynamic = folly::dynamic::array();
    for (const auto &type : newProps.allowedBarcodeTypes) {
      allowedBarcodeTypesDynamic.push_back(type);
    }
    id allowedBarcodeTypes =
        CKConvertFollyDynamicToId(allowedBarcodeTypesDynamic);
    if (allowedBarcodeTypes != nil &&
        [allowedBarcodeTypes isKindOfClass:NSArray.class]) {
      _view.allowedBarcodeTypes = allowedBarcodeTypes;
      [changedProps addObject:@"allowedBarcodeTypes"];
    }
  }

  [super updateProps:props oldProps:oldProps];
  [_view didSetProps:changedProps];
}

+ (BOOL)shouldBeRecycled {
  // Disable recycling as cameras are expensive to keep in memory and may cause
  // unintended behaviors (we need to reset the camera properly when recycling)
  // We can enable it later if find that the performance is needed
  return NO;
}

- (void)prepareForRecycle {
  [super prepareForRecycle];
  [self prepareView];
}

@end

Class<RCTComponentViewProtocol> CKCameraCls(void) {
  return CKCameraViewComponentView.class;
}

#endif // RCT_NEW_ARCH_ENABLED
