#import <objc/runtime.h>

#import <WebRTC/RTCCameraVideoCapturer.h>
#import <WebRTC/RTCMediaConstraints.h>
#import <WebRTC/RTCMediaStreamTrack.h>
#import <WebRTC/RTCVideoTrack.h>
#import <WebRTC/WebRTC.h>

#import "RTCMediaStreamTrack+React.h"
#import "WebRTCModule+RTCMediaStream.h"
#import "WebRTCModule+RTCPeerConnection.h"
#import "WebRTCModule+VideoTrackAdapter.h"
#import "WebRTCModuleOptions.h"

#import "InAppScreenCaptureController.h"
#import "InAppScreenCapturer.h"
#import "ProcessorProvider.h"
#import "ScreenCaptureController.h"
#import "ScreenCapturer.h"
#import "TrackCapturerEventsEmitter.h"

// Import Swift-generated header for ScreenShareAudioMixer
#if __has_include(<stream_react_native_webrtc/stream_react_native_webrtc-Swift.h>)
#import <stream_react_native_webrtc/stream_react_native_webrtc-Swift.h>
#elif __has_include("stream_react_native_webrtc-Swift.h")
#import "stream_react_native_webrtc-Swift.h"
#endif
#import "VideoCaptureController.h"

@implementation WebRTCModule (RTCMediaStream)

- (VideoEffectProcessor *)videoEffectProcessor {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setVideoEffectProcessor:(VideoEffectProcessor *)videoEffectProcessor {
    objc_setAssociatedObject(
        self, @selector(videoEffectProcessor), videoEffectProcessor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - Default Media Constraints

/**
 * Returns common optional constraints for improved audio quality.
 * Ported from stream-video-swift DefaultRTCMediaConstraints.
 * https://github.com/GetStream/stream-video-swift/blob/develop/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift
 */
+ (NSDictionary *)commonOptionalConstraints {
    return @{
        @"DtlsSrtpKeyAgreement" : kRTCMediaConstraintsValueTrue,
        @"googAutoGainControl" : kRTCMediaConstraintsValueTrue,
        @"googNoiseSuppression" : kRTCMediaConstraintsValueTrue,
        @"googEchoCancellation" : kRTCMediaConstraintsValueTrue,
        @"googHighpassFilter" : kRTCMediaConstraintsValueTrue,
        @"googTypingNoiseDetection" : kRTCMediaConstraintsValueTrue,
        @"googAudioMirroring" : kRTCMediaConstraintsValueFalse
    };
}

#pragma mark - getUserMedia

- (NSString *)convertBoolToString:(id)value {
    return value ? @"true" : @"false";
}

/**
 * Initializes a new {@link RTCAudioTrack} which satisfies the given constraints.
 *
 * @param constraints The {@code MediaStreamConstraints} which the new
 * {@code RTCAudioTrack} instance is to satisfy.
 */
- (RTCAudioTrack *)createAudioTrack:(NSDictionary *)constraints {
    NSString *trackId = [[NSUUID UUID] UUIDString];
    NSDictionary *audioConstraints = constraints[@"audio"];

    // Start with common optional constraints as defaults
    NSMutableDictionary *optionalConstraints = [[[self class] commonOptionalConstraints] mutableCopy];

    // Override with user-provided constraints if specified
    if (audioConstraints[@"autoGainControl"] != nil) {
        optionalConstraints[@"googAutoGainControl"] = [self convertBoolToString:audioConstraints[@"autoGainControl"]];
    }
    if (audioConstraints[@"noiseSuppression"] != nil) {
        optionalConstraints[@"googNoiseSuppression"] = [self convertBoolToString:audioConstraints[@"noiseSuppression"]];
    }
    if (audioConstraints[@"echoCancellation"] != nil) {
        optionalConstraints[@"googEchoCancellation"] = [self convertBoolToString:audioConstraints[@"echoCancellation"]];
    }
    if (audioConstraints[@"highpassFilter"] != nil) {
        optionalConstraints[@"googHighpassFilter"] = [self convertBoolToString:audioConstraints[@"highpassFilter"]];
    }

    RTCMediaConstraints *mediaConstraints =
        [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:optionalConstraints];

    RTCAudioSource *audioSource = [self.peerConnectionFactory audioSourceWithConstraints:mediaConstraints];
    RTCAudioTrack *audioTrack = [self.peerConnectionFactory audioTrackWithSource:audioSource trackId:trackId];
    return audioTrack;
}
/**
 * Initializes a new {@link RTCVideoTrack} with the given capture controller
 */
- (RTCVideoTrack *)createVideoTrackWithCaptureController:
    (CaptureController * (^)(RTCVideoSource *))captureControllerCreator {
#if TARGET_OS_TV
    return nil;
#else

    RTCVideoSource *videoSource = [self.peerConnectionFactory videoSource];

    NSString *trackUUID = [[NSUUID UUID] UUIDString];
    RTCVideoTrack *videoTrack = [self.peerConnectionFactory videoTrackWithSource:videoSource trackId:trackUUID];

    CaptureController *captureController = captureControllerCreator(videoSource);
    videoTrack.captureController = captureController;
    [captureController startCapture];

    // Add dimension detection for local video tracks immediately
    [self addLocalVideoTrackDimensionDetection:videoTrack];

    return videoTrack;
#endif
}
/**
 * Initializes a new {@link RTCMediaTrack} with the given tracks.
 *
 * @return An array with the mediaStreamId in index 0, and track infos in index 1.
 */
- (NSArray *)createMediaStream:(NSArray<RTCMediaStreamTrack *> *)tracks {
#if TARGET_OS_TV
    return nil;
#else
    NSString *mediaStreamId = [[NSUUID UUID] UUIDString];
    RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithStreamId:mediaStreamId];
    NSMutableArray<NSDictionary *> *trackInfos = [NSMutableArray array];

    for (RTCMediaStreamTrack *track in tracks) {
        if ([track.kind isEqualToString:@"audio"]) {
            [mediaStream addAudioTrack:(RTCAudioTrack *)track];
        } else if ([track.kind isEqualToString:@"video"]) {
            [mediaStream addVideoTrack:(RTCVideoTrack *)track];
        }

        NSString *trackId = track.trackId;

        self.localTracks[trackId] = track;

        NSDictionary *settings = @{};
        if ([track.kind isEqualToString:@"video"]) {
            RTCVideoTrack *videoTrack = (RTCVideoTrack *)track;
            if ([videoTrack.captureController isKindOfClass:[CaptureController class]]) {
                settings = [videoTrack.captureController getSettings];
            }
        } else if ([track.kind isEqualToString:@"audio"]) {
            settings = @{
                @"deviceId" : @"audio",
                @"groupId" : @"",
            };
        }

        [trackInfos addObject:@{
            @"enabled" : @(track.isEnabled),
            @"id" : trackId,
            @"kind" : track.kind,
            @"readyState" : @"live",
            @"remote" : @(NO),
            @"settings" : settings
        }];
    }

    self.localStreams[mediaStreamId] = mediaStream;
    return @[ mediaStreamId, trackInfos ];
#endif
}
/**
 * Initializes a new {@link RTCVideoTrack} which satisfies the given constraints.
 */
- (RTCVideoTrack *)createVideoTrack:(NSDictionary *)constraints {
#if TARGET_OS_TV
    return nil;
#else
    RTCVideoSource *videoSource = [self.peerConnectionFactory videoSource];

    NSString *trackUUID = [[NSUUID UUID] UUIDString];
    RTCVideoTrack *videoTrack = [self.peerConnectionFactory videoTrackWithSource:videoSource trackId:trackUUID];

    BOOL hasRuntimeVideoDevice = YES;
#if TARGET_IPHONE_SIMULATOR
    // On simulator, a runtime-provided video source may exist (e.g. virtual camera),
    // so only skip camera capture setup when no runtime video device is available.
    hasRuntimeVideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo] != nil;
#endif

    if (hasRuntimeVideoDevice) {
        RTCCameraVideoCapturer *videoCapturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource];
        VideoCaptureController *videoCaptureController =
            [[VideoCaptureController alloc] initWithCapturer:videoCapturer andConstraints:constraints[@"video"]];
        videoCaptureController.enableMultitaskingCameraAccess =
            [WebRTCModuleOptions sharedInstance].enableMultitaskingCameraAccess;
        videoTrack.captureController = videoCaptureController;
        [videoCaptureController startCapture];
    }

    // Add dimension detection for local video tracks immediately
    [self addLocalVideoTrackDimensionDetection:videoTrack];

    return videoTrack;
#endif
}

- (RTCVideoTrack *)createScreenCaptureVideoTrack {
#if TARGET_IPHONE_SIMULATOR || TARGET_OS_OSX || TARGET_OS_TV
    return nil;
#endif

    RTCVideoSource *videoSource = [self.peerConnectionFactory videoSourceForScreenCast:YES];

    NSString *trackUUID = [[NSUUID UUID] UUIDString];
    RTCVideoTrack *videoTrack = [self.peerConnectionFactory videoTrackWithSource:videoSource trackId:trackUUID];

    WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
    CaptureController *captureController;

    if (options.useInAppScreenCapture) {
        // Clear the flag so subsequent getDisplayMedia calls use broadcast by default
        options.useInAppScreenCapture = NO;

        InAppScreenCapturer *capturer = [[InAppScreenCapturer alloc] initWithDelegate:videoSource];
        InAppScreenCaptureController *controller = [[InAppScreenCaptureController alloc] initWithCapturer:capturer];

        // Store weak reference for audio mixing wiring
        options.activeInAppScreenCapturer = capturer;

        captureController = controller;
    } else {
        // Existing broadcast extension path
        ScreenCapturer *screenCapturer = [[ScreenCapturer alloc] initWithDelegate:videoSource];
        ScreenCaptureController *screenCaptureController =
            [[ScreenCaptureController alloc] initWithCapturer:screenCapturer];
        captureController = screenCaptureController;
    }

    TrackCapturerEventsEmitter *emitter = [[TrackCapturerEventsEmitter alloc] initWith:trackUUID webRTCModule:self];
    captureController.eventsDelegate = emitter;
    videoTrack.captureController = captureController;
    [captureController startCapture];

    // Add dimension detection for local video tracks immediately
    [self addLocalVideoTrackDimensionDetection:videoTrack];

    return videoTrack;
}

RCT_EXPORT_METHOD(getDisplayMedia
                  : (NSDictionary *)constraints resolver
                  : (RCTPromiseResolveBlock)resolve rejecter
                  : (RCTPromiseRejectBlock)reject) {
#if TARGET_OS_TV
    reject(@"unsupported_platform", @"tvOS is not supported", nil);
    return;
#else

    RTCVideoTrack *videoTrack = [self createScreenCaptureVideoTrack];

    if (videoTrack == nil) {
        reject(@"DOMException", @"AbortError", nil);
        return;
    }

    NSString *mediaStreamId = [[NSUUID UUID] UUIDString];
    RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithStreamId:mediaStreamId];
    [mediaStream addVideoTrack:videoTrack];

    NSString *trackId = videoTrack.trackId;
    self.localTracks[trackId] = videoTrack;

    NSDictionary *trackInfo = @{
        @"enabled" : @(videoTrack.isEnabled),
        @"id" : videoTrack.trackId,
        @"kind" : videoTrack.kind,
        @"readyState" : @"live",
        @"remote" : @(NO)
    };

    self.localStreams[mediaStreamId] = mediaStream;
    resolve(@{@"streamId" : mediaStreamId, @"track" : trackInfo});
#endif
}

/**
 * Implements {@code getUserMedia}. Note that at this point constraints have
 * been normalized and permissions have been granted. The constraints only
 * contain keys for which permissions have already been granted, that is,
 * if audio permission was not granted, there will be no "audio" key in
 * the constraints dictionary.
 */
RCT_EXPORT_METHOD(getUserMedia
                  : (NSDictionary *)constraints successCallback
                  : (RCTResponseSenderBlock)successCallback errorCallback
                  : (RCTResponseSenderBlock)errorCallback) {
#if TARGET_OS_TV
    errorCallback(@[ @"PlatformNotSupported", @"getUserMedia is not supported on tvOS." ]);
    return;
#else
    RTCAudioTrack *audioTrack = nil;
    RTCVideoTrack *videoTrack = nil;

    if (constraints[@"audio"]) {
        audioTrack = [self createAudioTrack:constraints];
        [self ensureAudioSessionWithRecording];
    }
    if (constraints[@"video"]) {
        videoTrack = [self createVideoTrack:constraints];
    }

    if (audioTrack == nil && videoTrack == nil) {
        // Fail with DOMException with name AbortError as per:
        // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia
        errorCallback(@[ @"DOMException", @"AbortError" ]);
        return;
    }

    NSString *mediaStreamId = [[NSUUID UUID] UUIDString];
    RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithStreamId:mediaStreamId];
    NSMutableArray *tracks = [NSMutableArray array];
    NSMutableArray *tmp = [NSMutableArray array];
    if (audioTrack)
        [tmp addObject:audioTrack];
    if (videoTrack)
        [tmp addObject:videoTrack];

    for (RTCMediaStreamTrack *track in tmp) {
        if ([track.kind isEqualToString:@"audio"]) {
            [mediaStream addAudioTrack:(RTCAudioTrack *)track];
        } else if ([track.kind isEqualToString:@"video"]) {
            [mediaStream addVideoTrack:(RTCVideoTrack *)track];
        }

        NSString *trackId = track.trackId;

        self.localTracks[trackId] = track;

        NSDictionary *settings = @{};
        if ([track.kind isEqualToString:@"video"]) {
            RTCVideoTrack *videoTrack = (RTCVideoTrack *)track;
            if ([videoTrack.captureController isKindOfClass:[CaptureController class]]) {
                settings = [videoTrack.captureController getSettings];
            }
        } else if ([track.kind isEqualToString:@"audio"]) {
            settings = @{
                @"deviceId" : @"audio",
                @"groupId" : @"",
            };
        }

        [tracks addObject:@{
            @"enabled" : @(track.isEnabled),
            @"id" : trackId,
            @"kind" : track.kind,
            @"readyState" : @"live",
            @"remote" : @(NO),
            @"settings" : settings
        }];
    }

    self.localStreams[mediaStreamId] = mediaStream;
    successCallback(@[ mediaStreamId, tracks ]);
#endif
}

#pragma mark - enumerateDevices

RCT_EXPORT_METHOD(enumerateDevices : (RCTResponseSenderBlock)callback) {
#if TARGET_OS_TV
    callback(@[]);
#else
    NSMutableArray *devices = [NSMutableArray array];
    NSMutableArray *deviceTypes = [NSMutableArray array];
    [deviceTypes addObjectsFromArray:@[
        AVCaptureDeviceTypeBuiltInWideAngleCamera,
        AVCaptureDeviceTypeBuiltInUltraWideCamera,
        AVCaptureDeviceTypeBuiltInTelephotoCamera,
        AVCaptureDeviceTypeBuiltInDualCamera,
        AVCaptureDeviceTypeBuiltInDualWideCamera,
        AVCaptureDeviceTypeBuiltInTripleCamera
    ]];
    if (@available(macos 14.0, ios 17.0, tvos 17.0, *)) {
        [deviceTypes addObject:AVCaptureDeviceTypeExternal];
    }
    AVCaptureDeviceDiscoverySession *videoDevicesSession =
        [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
                                                               mediaType:AVMediaTypeVideo
                                                                position:AVCaptureDevicePositionUnspecified];
    for (AVCaptureDevice *device in videoDevicesSession.devices) {
        if (device.uniqueID == nil) {
            continue;
        }
        NSString *position = @"unknown";
        if (device.position == AVCaptureDevicePositionBack) {
            position = @"environment";
        } else if (device.position == AVCaptureDevicePositionFront) {
            position = @"front";
        }
        NSString *label = @"Unknown video device";
        if (device.localizedName != nil) {
            label = device.localizedName;
        }

        [devices addObject:@{
            @"facing" : position,
            @"deviceId" : device.uniqueID,
            @"groupId" : @"",
            @"label" : label,
            @"kind" : @"videoinput",
        }];
    }

    AVCaptureDeviceDiscoverySession *audioDevicesSession =
        [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[ AVCaptureDeviceTypeBuiltInMicrophone ]
                                                               mediaType:AVMediaTypeAudio
                                                                position:AVCaptureDevicePositionUnspecified];
    for (AVCaptureDevice *device in audioDevicesSession.devices) {
        if (device.uniqueID == nil) {
            continue;
        }
        NSString *label = @"Unknown audio device";
        if (device.localizedName != nil) {
            label = device.localizedName;
        }
        [devices addObject:@{
            @"deviceId" : device.uniqueID,
            @"groupId" : @"",
            @"label" : label,
            @"kind" : @"audioinput",
        }];
    }
    callback(@[ devices ]);
#endif
}

#pragma mark - Local Video Track Dimension Detection

- (void)addLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack {
    if (!videoTrack) {
        return;
    }

    // Create a dimension detector for this local track
    VideoDimensionDetector *detector = [[VideoDimensionDetector alloc] initWith:@(-1)  // -1 for local tracks
                                                                        trackId:videoTrack.trackId
                                                                   webRTCModule:self];

    // Store the detector using associated objects on the track itself
    objc_setAssociatedObject(
        videoTrack, @selector(addLocalVideoTrackDimensionDetection:), detector, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // Add the detector as a renderer to the track
    [videoTrack addRenderer:detector];

    RCTLogTrace(@"[VideoTrackAdapter] Local dimension detector created for track %@", videoTrack.trackId);
}

- (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack {
    if (!videoTrack) {
        return;
    }

    // Get the associated detector
    VideoDimensionDetector *detector =
        objc_getAssociatedObject(videoTrack, @selector(addLocalVideoTrackDimensionDetection:));

    if (detector) {
        [videoTrack removeRenderer:detector];
        [detector dispose];
        objc_setAssociatedObject(
            videoTrack, @selector(addLocalVideoTrackDimensionDetection:), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        RCTLogTrace(@"[VideoTrackAdapter] Local dimension detector removed for track %@", videoTrack.trackId);
    }
}

#pragma mark - Other stream related APIs

RCT_EXPORT_METHOD(mediaStreamCreate : (nonnull NSString *)streamID) {
    RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithStreamId:streamID];
    self.localStreams[streamID] = mediaStream;
}

RCT_EXPORT_METHOD(mediaStreamAddTrack
                  : (nonnull NSString *)streamID
                  : (nonnull NSNumber *)pcId
                  : (nonnull NSString *)trackID) {
    RTCMediaStream *mediaStream = self.localStreams[streamID];
    if (mediaStream == nil) {
        return;
    }

    RTCMediaStreamTrack *track = [self trackForId:trackID pcId:pcId];
    if (track == nil) {
        return;
    }

    if ([track.kind isEqualToString:@"audio"]) {
        [mediaStream addAudioTrack:(RTCAudioTrack *)track];
    } else if ([track.kind isEqualToString:@"video"]) {
        [mediaStream addVideoTrack:(RTCVideoTrack *)track];
    }
}

RCT_EXPORT_METHOD(mediaStreamRemoveTrack
                  : (nonnull NSString *)streamID
                  : (nonnull NSNumber *)pcId
                  : (nonnull NSString *)trackID) {
    RTCMediaStream *mediaStream = self.localStreams[streamID];
    if (mediaStream == nil) {
        return;
    }

    RTCMediaStreamTrack *track = [self trackForId:trackID pcId:pcId];
    if (track == nil) {
        return;
    }

    if ([track.kind isEqualToString:@"audio"]) {
        [mediaStream removeAudioTrack:(RTCAudioTrack *)track];
    } else if ([track.kind isEqualToString:@"video"]) {
        [mediaStream removeVideoTrack:(RTCVideoTrack *)track];
    }
}

RCT_EXPORT_METHOD(mediaStreamRelease : (nonnull NSString *)streamID) {
    RTCMediaStream *stream = self.localStreams[streamID];
    if (stream) {
        [self.localStreams removeObjectForKey:streamID];
    }
}

RCT_EXPORT_METHOD(mediaStreamTrackRelease : (nonnull NSString *)trackID) {
#if TARGET_OS_TV
    return;
#else

    RTCMediaStreamTrack *track = self.localTracks[trackID];
    if (track) {
        if ([track.kind isEqualToString:@"video"]) {
            // Clean up dimension detection for local video tracks
            [self removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)track];
        }
        if (track.isEnabled) {
            track.isEnabled = NO;
        }
        [track.captureController stopCapture];
        [self.localTracks removeObjectForKey:trackID];
    }
#endif
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(mediaStreamTrackClone : (nonnull NSString *)trackID) {
#if TARGET_OS_TV
    return;
#else

    RTCMediaStreamTrack *originalTrack = self.localTracks[trackID];
    if (originalTrack) {
        NSString *trackUUID = [[NSUUID UUID] UUIDString];
        if ([originalTrack.kind isEqualToString:@"audio"]) {
            RTCAudioTrack *audioTrack = [self.peerConnectionFactory audioTrackWithTrackId:trackUUID];
            audioTrack.isEnabled = originalTrack.isEnabled;
            [self.localTracks setObject:audioTrack forKey:trackUUID];
            for (NSString *streamId in [self.localStreams allKeys]) {
                RTCMediaStream *stream = [self.localStreams objectForKey:streamId];
                if (stream == nil)
                    continue;
                for (RTCAudioTrack *track in [stream.audioTracks copy]) {
                    if ([trackID isEqualToString:track.trackId]) {
                        [stream addAudioTrack:audioTrack];
                    }
                }
            }
        } else {
            RTCVideoTrack *originalVideoTrack = (RTCVideoTrack *)originalTrack;
            RTCVideoSource *videoSource = originalVideoTrack.source;
            RTCVideoTrack *videoTrack = [self.peerConnectionFactory videoTrackWithSource:videoSource trackId:trackUUID];
            videoTrack.isEnabled = originalTrack.isEnabled;

            // Add dimension detection for cloned local video tracks
            [self addLocalVideoTrackDimensionDetection:videoTrack];

            [self.localTracks setObject:videoTrack forKey:trackUUID];
            for (NSString *streamId in [self.localStreams allKeys]) {
                RTCMediaStream *stream = [self.localStreams objectForKey:streamId];
                if (stream == nil)
                    continue;
                for (RTCVideoTrack *track in [stream.videoTracks copy]) {
                    if ([trackID isEqualToString:track.trackId]) {
                        [stream addVideoTrack:videoTrack];
                    }
                }
            }
        }
        return trackUUID;
    }
    return @"";
#endif
}

RCT_EXPORT_METHOD(mediaStreamTrackSetEnabled : (nonnull NSNumber *)pcId : (nonnull NSString *)trackID : (BOOL)enabled) {
    RTCMediaStreamTrack *track = [self trackForId:trackID pcId:pcId];
    if (track == nil) {
        return;
    }

    track.isEnabled = enabled;

#if !TARGET_OS_TV
    if (track.captureController) {  // It could be a remote track!
        if (enabled) {
            [track.captureController startCapture];
        } else {
            [track.captureController stopCapture];
        }
    }
#endif
}

RCT_EXPORT_METHOD(mediaStreamTrackApplyConstraints
                  : (nonnull NSString *)trackID
                  : (NSDictionary *)constraints
                  : (RCTPromiseResolveBlock)resolve
                  : (RCTPromiseRejectBlock)reject) {
#if TARGET_OS_TV
    reject(@"unsupported_platform", @"tvOS is not supported", nil);
    return;
#else
    RTCMediaStreamTrack *track = self.localTracks[trackID];
    if (track) {
        if ([track.kind isEqualToString:@"video"]) {
            RTCVideoTrack *videoTrack = (RTCVideoTrack *)track;
            if ([videoTrack.captureController isKindOfClass:[CaptureController class]]) {
                CaptureController *vcc = (CaptureController *)videoTrack.captureController;
                NSError *error = nil;
                [vcc applyConstraints:constraints error:&error];
                if (error) {
                    reject(@"E_INVALID", error.localizedDescription, error);
                } else {
                    resolve([vcc getSettings]);
                }
            }
        } else {
            RCTLogWarn(@"mediaStreamTrackApplyConstraints() track is not video");
            reject(@"E_INVALID", @"Can't apply constraints on audio tracks", nil);
        }
    } else {
        RCTLogWarn(@"mediaStreamTrackApplyConstraints() track is null");
        reject(@"E_INVALID", @"Could not get track", nil);
    }
#endif
}

RCT_EXPORT_METHOD(mediaStreamTrackSetVolume : (nonnull NSNumber *)pcId : (nonnull NSString *)trackID : (double)volume) {
    RTCMediaStreamTrack *track = [self trackForId:trackID pcId:pcId];
    if (track && [track.kind isEqualToString:@"audio"]) {
        RTCAudioTrack *audioTrack = (RTCAudioTrack *)track;
        audioTrack.source.volume = volume;
    }
}

RCT_EXPORT_METHOD(mediaStreamTrackSetVideoEffects
                  : (nonnull NSString *)trackID names
                  : (nonnull NSArray<NSString *> *)names) {
    RTCMediaStreamTrack *track = self.localTracks[trackID];
    if (track) {
        RTCVideoTrack *videoTrack = (RTCVideoTrack *)track;
        RTCVideoSource *videoSource = videoTrack.source;

        NSMutableArray *processors = [[NSMutableArray alloc] init];
        for (NSString *name in names) {
            NSObject<VideoFrameProcessorDelegate> *processor = [ProcessorProvider getProcessor:name];
            if (processor != nil) {
                [processors addObject:processor];
            }
        }

        self.videoEffectProcessor = [[VideoEffectProcessor alloc] initWithProcessors:processors
                                                                         videoSource:videoSource];

        VideoCaptureController *vcc = (VideoCaptureController *)videoTrack.captureController;
        RTCVideoCapturer *capturer = vcc.capturer;

        capturer.delegate = self.videoEffectProcessor;
    }
}

#pragma mark - Helpers

- (RTCMediaStreamTrack *)trackForId:(nonnull NSString *)trackId pcId:(nonnull NSNumber *)pcId {
    if ([pcId isEqualToNumber:[NSNumber numberWithInt:-1]]) {
        return self.localTracks[trackId];
    }

    RTCPeerConnection *peerConnection = self.peerConnections[pcId];
    if (peerConnection == nil) {
        return nil;
    }

    return peerConnection.remoteTracks[trackId];
}

- (void)ensureAudioSessionWithRecording {
    RTCAudioSession *session = [RTCAudioSession sharedInstance];

    // we also need to set default WebRTC audio configuration, since it may be activated after
    // this method is called
    RTCAudioSessionConfiguration *config = [RTCAudioSessionConfiguration webRTCConfiguration];
    // require audio session to be either PlayAndRecord or MultiRoute
    if (session.category != AVAudioSessionCategoryPlayAndRecord) {
        [session lockForConfiguration];
        config.category = AVAudioSessionCategoryPlayAndRecord;
        config.categoryOptions =
            AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker;
        config.mode = AVAudioSessionModeVideoChat;
        NSError *error = nil;
        bool success = [session setCategory:config.category withOptions:config.categoryOptions error:&error];
        if (!success) {
            NSLog(@"ensureAudioSessionWithRecording: setCategory failed due to: %@", [error localizedDescription]);
        }
        success = [session setMode:config.mode error:&error];
        if (!success) {
            NSLog(@"ensureAudioSessionWithRecording: Error setting category: %@", [error localizedDescription]);
        }
        [session unlockForConfiguration];
    }
}

@end
