#import "RNSBottomTabsScreenComponentView.h"
#import "NSString+RNSUtility.h"
#import "RNSConversions.h"
#import "RNSDefines.h"
#import "RNSLog.h"
#import "RNSScrollViewHelper.h"
#import "RNSTabBarAppearanceCoordinator.h"
#import "RNSTabBarController.h"

#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTImageSource.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#endif // RCT_NEW_ARCH_ENABLED

#if RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED

#pragma mark - View implementation

@implementation RNSBottomTabsScreenComponentView {
  RNSTabsScreenViewController *_controller;
  RNSBottomTabsHostComponentView *__weak _Nullable _reactSuperview;

  RNSBottomTabsScreenEventEmitter *_Nonnull _reactEventEmitter;

  // We need this information to warn users about dynamic changes to behavior being currently unsupported.
  BOOL _isOverrideScrollViewContentInsetAdjustmentBehaviorSet;
#if !RCT_NEW_ARCH_ENABLED
  BOOL _tabItemNeedsAppearanceUpdate;
  BOOL _tabScreenOrientationNeedsUpdate;
  BOOL _tabBarItemNeedsUpdate;
#endif // !RCT_NEW_ARCH_ENABLED
}

- (instancetype)initWithFrame:(CGRect)frame
{
  if (self = [super initWithFrame:frame]) {
    [self initState];
  }
  return self;
}

- (void)initState
{
#if RCT_NEW_ARCH_ENABLED
  static const auto defaultProps = std::make_shared<const react::RNSBottomTabsScreenProps>();
  _props = defaultProps;
#endif // RCT_NEW_ARCH_ENABLED

  _controller = [RNSTabsScreenViewController new];
  _controller.view = self;

  _reactSuperview = nil;
  _reactEventEmitter = [RNSBottomTabsScreenEventEmitter new];

#if !RCT_NEW_ARCH_ENABLED
  _tabItemNeedsAppearanceUpdate = NO;
  _tabScreenOrientationNeedsUpdate = NO;
  _tabBarItemNeedsUpdate = NO;
#endif

  // This is a temporary workaround to avoid UIScrollEdgeEffect glitch
  // when changing tabs when ScrollView is present.
  // TODO: don't hardcode color here
  self.backgroundColor = [UIColor whiteColor];

  [self resetProps];
}

- (void)resetProps
{
  _isSelectedScreen = NO;
  _badgeValue = nil;
  _title = nil;
  _orientation = RNSOrientationInherit;

  _standardAppearance = [UITabBarAppearance new];
  _scrollEdgeAppearance = nil;

  _shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
  _shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES;

  _overrideScrollViewContentInsetAdjustmentBehavior = YES;
  _isOverrideScrollViewContentInsetAdjustmentBehaviorSet = NO;

  _iconType = RNSBottomTabsIconTypeSfSymbol;

  _iconImageSource = nil;
  _iconSfSymbolName = nil;

  _selectedIconImageSource = nil;
  _selectedIconSfSymbolName = nil;

  _systemItem = RNSBottomTabsScreenSystemItemNone;
}

RNS_IGNORE_SUPER_CALL_BEGIN
- (nullable RNSBottomTabsHostComponentView *)reactSuperview
{
  return _reactSuperview;
}
RNS_IGNORE_SUPER_CALL_END

#ifdef RCT_NEW_ARCH_ENABLED

#pragma mark - RNSViewControllerInvalidating

- (void)invalidateController
{
  _controller = nil;
}

#else

#pragma mark - RCTInvalidating

- (void)invalidate
{
  _controller = nil;
}

#endif

#pragma mark - Events

- (nonnull RNSBottomTabsScreenEventEmitter *)reactEventEmitter
{
  RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
  return _reactEventEmitter;
}

- (nullable RNSTabBarController *)findTabBarController
{
  return static_cast<RNSTabBarController *>(_controller.tabBarController);
}

#pragma mark - RNSScrollViewBehaviorOverriding

- (BOOL)shouldOverrideScrollViewContentInsetAdjustmentBehavior
{
  return self.overrideScrollViewContentInsetAdjustmentBehavior;
}

- (void)overrideScrollViewBehaviorInFirstDescendantChainIfNeeded
{
  if ([self shouldOverrideScrollViewContentInsetAdjustmentBehavior]) {
    [RNSScrollViewHelper overrideScrollViewBehaviorInFirstDescendantChainFrom:self];
  }
}

#pragma mark - Prop update utils

- (void)updateTabBarItem
{
  UITabBarItem *tabBarItem = nil;
  if (_systemItem != RNSBottomTabsScreenSystemItemNone) {
    UITabBarSystemItem systemItem =
        rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(_systemItem);
    tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0];
  } else {
    tabBarItem = [[UITabBarItem alloc] init];
    tabBarItem.title = _title;
  }

  tabBarItem.badgeValue = _badgeValue;

  _controller.tabBarItem = tabBarItem;
}

#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTViewComponentViewProtocol

- (void)updateProps:(const facebook::react::Props::Shared &)props
           oldProps:(const facebook::react::Props::Shared &)oldProps
{
  const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(_props);
  const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(props);

  bool tabItemNeedsAppearanceUpdate{false};
  bool tabScreenOrientationNeedsUpdate{false};
  bool tabBarItemNeedsUpdate{false};

  if (newComponentProps.title != oldComponentProps.title) {
    _title = RCTNSStringFromStringNilIfEmpty(newComponentProps.title);
    _controller.title = _title;
  }

  if (newComponentProps.orientation != oldComponentProps.orientation) {
    _orientation =
        rnscreens::conversion::RNSOrientationFromRNSBottomTabsScreenOrientation(newComponentProps.orientation);
    tabScreenOrientationNeedsUpdate = YES;
  }

  if (newComponentProps.tabKey != oldComponentProps.tabKey) {
    RCTAssert(!newComponentProps.tabKey.empty(), @"[RNScreens] tabKey must not be empty!");
    _tabKey = RCTNSStringFromString(newComponentProps.tabKey);
  }

  if (newComponentProps.isFocused != oldComponentProps.isFocused) {
    _isSelectedScreen = newComponentProps.isFocused;
    [_controller tabScreenFocusHasChanged];
  }

  if (newComponentProps.badgeValue != oldComponentProps.badgeValue) {
    _badgeValue = RCTNSStringFromStringNilIfEmpty(newComponentProps.badgeValue);
    tabBarItemNeedsUpdate = YES;
  }

  if (newComponentProps.standardAppearance != oldComponentProps.standardAppearance) {
    _standardAppearance = [UITabBarAppearance new];
    [RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance
                                          fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId(
                                                                  newComponentProps.standardAppearance)];
    tabItemNeedsAppearanceUpdate = YES;
  }

  if (newComponentProps.scrollEdgeAppearance != oldComponentProps.scrollEdgeAppearance) {
    if (newComponentProps.scrollEdgeAppearance.type() == folly::dynamic::OBJECT) {
      _scrollEdgeAppearance = [UITabBarAppearance new];
      [RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance
                                            fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId(
                                                                    newComponentProps.scrollEdgeAppearance)];
    } else {
      _scrollEdgeAppearance = nil;
    }
    tabItemNeedsAppearanceUpdate = YES;
  }

  if (newComponentProps.iconType != oldComponentProps.iconType) {
    _iconType = rnscreens::conversion::RNSBottomTabsIconTypeFromIcon(newComponentProps.iconType);
    tabItemNeedsAppearanceUpdate = YES;
    tabBarItemNeedsUpdate = YES;
  }

  if (newComponentProps.iconImageSource != oldComponentProps.iconImageSource) {
    _iconImageSource =
        rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(&newComponentProps.iconImageSource, _iconType);
    tabItemNeedsAppearanceUpdate = YES;
    tabBarItemNeedsUpdate = YES;
  }

  if (newComponentProps.iconSfSymbolName != oldComponentProps.iconSfSymbolName) {
    _iconSfSymbolName = RCTNSStringFromStringNilIfEmpty(newComponentProps.iconSfSymbolName);
    tabItemNeedsAppearanceUpdate = YES;
    tabBarItemNeedsUpdate = YES;
  }

  if (newComponentProps.selectedIconImageSource != oldComponentProps.selectedIconImageSource) {
    _selectedIconImageSource = rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(
        &newComponentProps.selectedIconImageSource, _iconType);
    tabItemNeedsAppearanceUpdate = YES;
    tabBarItemNeedsUpdate = YES;
  }

  if (newComponentProps.selectedIconSfSymbolName != oldComponentProps.selectedIconSfSymbolName) {
    _selectedIconSfSymbolName = RCTNSStringFromStringNilIfEmpty(newComponentProps.selectedIconSfSymbolName);
    tabItemNeedsAppearanceUpdate = YES;
    tabBarItemNeedsUpdate = YES;
  }

  if (newComponentProps.specialEffects.repeatedTabSelection.popToRoot !=
      oldComponentProps.specialEffects.repeatedTabSelection.popToRoot) {
    _shouldUseRepeatedTabSelectionPopToRootSpecialEffect =
        newComponentProps.specialEffects.repeatedTabSelection.popToRoot;
  }

  if (newComponentProps.specialEffects.repeatedTabSelection.scrollToTop !=
      oldComponentProps.specialEffects.repeatedTabSelection.scrollToTop) {
    _shouldUseRepeatedTabSelectionScrollToTopSpecialEffect =
        newComponentProps.specialEffects.repeatedTabSelection.scrollToTop;
  }

  if (newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior !=
      oldComponentProps.overrideScrollViewContentInsetAdjustmentBehavior) {
    _overrideScrollViewContentInsetAdjustmentBehavior =
        newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior;

    if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) {
      RCTLogWarn(
          @"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported");
    }
  }

  // This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop
  // is assigned for the first time. This allows us to identify any subsequent changes to this prop,
  // enabling us to warn users that dynamic changes are not supported.
  _isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES;

  if (newComponentProps.systemItem != oldComponentProps.systemItem) {
    _systemItem = rnscreens::conversion::RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem(
        newComponentProps.systemItem);
    tabBarItemNeedsUpdate = YES;
  }

  if (tabBarItemNeedsUpdate) {
    [self updateTabBarItem];

    // Force appearance update to make sure correct image for tab bar item is used
    tabItemNeedsAppearanceUpdate = YES;
  }

  if (tabItemNeedsAppearanceUpdate) {
    [_controller tabItemAppearanceHasChanged];
  }

  if (tabScreenOrientationNeedsUpdate) {
    [_controller tabScreenOrientationHasChanged];
  }

  [super updateProps:props oldProps:oldProps];
}

- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
           oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics
{
  RNSLog(
      @"TabScreen [%ld] updateLayoutMetrics: %@", self.tag, NSStringFromCGRect(RCTCGRectFromRect(layoutMetrics.frame)));
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
}

- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
  [super updateEventEmitter:eventEmitter];
  [_reactEventEmitter
      updateEventEmitter:std::static_pointer_cast<const react::RNSBottomTabsScreenEventEmitter>(eventEmitter)];
}

- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
  RNSLog(@"TabScreen [%ld] mount [%ld] at %ld", self.tag, childComponentView.tag, index);
  [super mountChildComponentView:childComponentView index:index];
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
  RNSLog(@"TabScreen [%ld] unmount [%ld] from %ld", self.tag, childComponentView.tag, index);
  [super unmountChildComponentView:childComponentView index:index];
}

+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
  return react::concreteComponentDescriptorProvider<react::RNSBottomTabsScreenComponentDescriptor>();
}

+ (BOOL)shouldBeRecycled
{
  // There won't be tens of instances of this component usually & it's easier for now.
  // We could consider enabling it someday though.
  return NO;
}

#else

#pragma mark - LEGACY RCTComponent protocol

- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
  [super didSetProps:changedProps];

  // This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop
  // is assigned for the first time. This allows us to identify any subsequent changes to this prop,
  // enabling us to warn users that dynamic changes are not supported.
  // On Paper, setter for the prop may not be called (when it is undefined in JS).
  // Therefore we set the flag in didSetProps to make sure to handle this case as well.
  // didSetProps will always be called because tabKey prop is required.
  _isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES;

  if (_tabBarItemNeedsUpdate) {
    [self updateTabBarItem];
    _tabBarItemNeedsUpdate = NO;

    // Force appearance update to make sure correct image for tab bar item is used
    _tabItemNeedsAppearanceUpdate = YES;
  }

  if (_tabItemNeedsAppearanceUpdate) {
    [_controller tabItemAppearanceHasChanged];
    _tabItemNeedsAppearanceUpdate = NO;
  }

  if (_tabScreenOrientationNeedsUpdate) {
    [_controller tabScreenOrientationHasChanged];
    _tabScreenOrientationNeedsUpdate = NO;
  }
}

#pragma mark - LEGACY prop setters

- (void)setIsSelectedScreen:(BOOL)isSelectedScreen
{
  if (_isSelectedScreen != isSelectedScreen) {
    _isSelectedScreen = isSelectedScreen;
    [_controller tabScreenFocusHasChanged];
  }
}

- (void)setTabKey:(NSString *)tabKey
{
  RCTAssert([NSString rnscreens_isBlankOrNull:tabKey] == NO, @"[RNScreens] tabKey must not be empty");
  _tabKey = tabKey;
}

- (void)setTitle:(NSString *)title
{
  _title = title;
  _controller.title = title;
}

- (void)setBadgeValue:(NSString *)badgeValue
{
  _badgeValue = [NSString rnscreens_stringOrNilIfBlank:badgeValue];
  _tabBarItemNeedsUpdate = YES;
}

- (void)setIconType:(RNSBottomTabsIconType)iconType
{
  _iconType = iconType;
  _tabItemNeedsAppearanceUpdate = YES;
  _tabBarItemNeedsUpdate = YES;
}

- (void)setIconImageSource:(RCTImageSource *)iconImageSource
{
  _iconImageSource = iconImageSource;
  _tabItemNeedsAppearanceUpdate = YES;
  _tabBarItemNeedsUpdate = YES;
}

- (void)setIconSfSymbolName:(NSString *)iconSfSymbolName
{
  _iconSfSymbolName = [NSString rnscreens_stringOrNilIfEmpty:iconSfSymbolName];
  _tabItemNeedsAppearanceUpdate = YES;
  _tabBarItemNeedsUpdate = YES;
}

- (void)setSelectedIconImageSource:(RCTImageSource *)selectedIconImageSource
{
  _selectedIconImageSource = selectedIconImageSource;
  _tabItemNeedsAppearanceUpdate = YES;
  _tabBarItemNeedsUpdate = YES;
}

- (void)setSelectedIconSfSymbolName:(NSString *)selectedIconSfSymbolName
{
  _selectedIconSfSymbolName = [NSString rnscreens_stringOrNilIfEmpty:selectedIconSfSymbolName];
  _tabItemNeedsAppearanceUpdate = YES;
  _tabBarItemNeedsUpdate = YES;
}

- (void)setOverrideScrollViewContentInsetAdjustmentBehavior:(BOOL)overrideScrollViewContentInsetAdjustmentBehavior
{
  _overrideScrollViewContentInsetAdjustmentBehavior = overrideScrollViewContentInsetAdjustmentBehavior;

  if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) {
    RCTLogWarn(
        @"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported");
  }

  // _isOverrideScrollViewContentInsetAdjustmentBehaviorSet flag is set in didSetProps to handle a case
  // when the prop is undefined in JS and default value is used instead of calling this setter.
}

- (void)setStandardAppearance:(NSDictionary *)standardAppearanceProps
{
  _standardAppearance = [UITabBarAppearance new];
  if (standardAppearanceProps != nil) {
    [RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance
                                          fromAppearanceProps:standardAppearanceProps];
  }
  _tabItemNeedsAppearanceUpdate = YES;
}

- (void)setScrollEdgeAppearance:(NSDictionary *)scrollEdgeAppearanceProps
{
  if (scrollEdgeAppearanceProps != nil) {
    _scrollEdgeAppearance = [UITabBarAppearance new];
    [RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance
                                          fromAppearanceProps:scrollEdgeAppearanceProps];
  } else {
    _scrollEdgeAppearance = nil;
  }
  _tabItemNeedsAppearanceUpdate = YES;
}

// This is a Paper-only setter method that will be called by the mounting code.
// It allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input from JS.
- (void)setSystemItem:(RNSBottomTabsScreenSystemItem)systemItem
{
  _systemItem = systemItem;
  _tabBarItemNeedsUpdate = YES;
}

- (void)setOrientation:(RNSOrientation)orientation
{
  _orientation = orientation;
  _tabScreenOrientationNeedsUpdate = YES;
}

- (void)setOnWillAppear:(RCTDirectEventBlock)onWillAppear
{
  [self.reactEventEmitter setOnWillAppear:onWillAppear];
}

- (void)setOnWillDisappear:(RCTDirectEventBlock)onWillDisappear
{
  [self.reactEventEmitter setOnWillDisappear:onWillDisappear];
}

- (void)setOnDidAppear:(RCTDirectEventBlock)onDidAppear
{
  [self.reactEventEmitter setOnDidAppear:onDidAppear];
}

- (void)setOnDidDisappear:(RCTDirectEventBlock)onDidDisappear
{
  [self.reactEventEmitter setOnDidDisappear:onDidDisappear];
}

#define RNS_FAILING_EVENT_GETTER(eventName)                                           \
  -(RCTDirectEventBlock)eventName                                                     \
  {                                                                                   \
    RCTAssert(NO, @"[RNScreens] Events should be emitted through reactEventEmitter"); \
    return nil;                                                                       \
  }

RNS_FAILING_EVENT_GETTER(onWillAppear);
RNS_FAILING_EVENT_GETTER(onDidAppear);
RNS_FAILING_EVENT_GETTER(onWillDisappear);
RNS_FAILING_EVENT_GETTER(onDidDisappear);

#undef RNS_FAILING_EVENT_GETTER

#endif // RCT_NEW_ARCH_ENABLED

@end

#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure

Class<RCTComponentViewProtocol> RNSBottomTabsScreen(void)
{
  return RNSBottomTabsScreenComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED
