package com.oney.WebRTCModule;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.projection.MediaProjectionConfig;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.Log;

import androidx.core.util.Consumer;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.BaseActivityEventListener;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.oney.WebRTCModule.videoEffects.ProcessorProvider;
import com.oney.WebRTCModule.videoEffects.VideoEffectProcessor;
import com.oney.WebRTCModule.videoEffects.VideoFrameProcessor;

import org.webrtc.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * The implementation of {@code getUserMedia} extracted into a separate file in
 * order to reduce complexity and to (somewhat) separate concerns.
 */
public class GetUserMediaImpl {
    /**
     * The {@link Log} tag with which {@code GetUserMediaImpl} is to log.
     */
    private static final String TAG = WebRTCModule.TAG;

    private static final int PERMISSION_REQUEST_CODE = (int) (Math.random() * Short.MAX_VALUE);

    private CameraEnumerator cameraEnumerator;
    private final ReactApplicationContext reactContext;

    /**
     * The application/library-specific private members of local
     * {@link MediaStreamTrack}s created by {@code GetUserMediaImpl} mapped by
     * track ID.
     */
    private final Map<String, TrackPrivate> tracks = new HashMap<>();

    private final WebRTCModule webRTCModule;

    private Promise displayMediaPromise;
    private Intent mediaProjectionPermissionResultData;
    private boolean createConfigForDefaultDisplay = false;
    private float resolutionScale = 1.0f;

    /**
     * Returns the MediaProjection permission result data Intent.
     * This Intent can be used to create a MediaProjection for audio capture
     * via AudioPlaybackCaptureConfiguration.
     */
    public Intent getMediaProjectionPermissionResultData() {
        return mediaProjectionPermissionResultData;
    }

    private final ServiceConnection mediaProjectionServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // Service is now bound, you can call createScreenStream()
            Log.d(TAG, "MediaProjectionService bound, creating screen stream.");
            ThreadUtils.runOnExecutor(() -> { createScreenStream(); });
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG, "MediaProjectionService disconnected.");
        }
    };

    GetUserMediaImpl(WebRTCModule webRTCModule, ReactApplicationContext reactContext) {
        this.webRTCModule = webRTCModule;
        this.reactContext = reactContext;

        reactContext.addActivityEventListener(new BaseActivityEventListener() {
            @Override
            public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
                super.onActivityResult(activity, requestCode, resultCode, data);
                if (requestCode == PERMISSION_REQUEST_CODE) {
                    if (resultCode != Activity.RESULT_OK) {
                        displayMediaPromise.reject("DOMException", "NotAllowedError");
                        displayMediaPromise = null;
                        return;
                    }

                    mediaProjectionPermissionResultData = data;

                    MediaProjectionService.launch(activity, mediaProjectionServiceConnection);
                }
            }
        });
    }

    private AudioTrack createAudioTrack(ReadableMap constraints) {
        ReadableMap audioConstraintsMap = constraints.getMap("audio");

        Log.d(TAG, "getUserMedia(audio): " + audioConstraintsMap);

        String id = UUID.randomUUID().toString();
        PeerConnectionFactory pcFactory = webRTCModule.mFactory;
        MediaConstraints peerConstraints = webRTCModule.constraintsForOptions(audioConstraintsMap);

        // Convert given constraints into the internal webrtc media constraints.
        peerConstraints.optional.add(new MediaConstraints.KeyValuePair("googAutoGainControl",
                audioConstraintsMap.hasKey("autoGainControl")
                        ? ReactBridgeUtil.getMapStrValue(audioConstraintsMap, "autoGainControl")
                        : "true"));
        peerConstraints.optional.add(new MediaConstraints.KeyValuePair("googNoiseSuppression",
                audioConstraintsMap.hasKey("noiseSuppression")
                        ? ReactBridgeUtil.getMapStrValue(audioConstraintsMap, "noiseSuppression")
                        : "true"));
        peerConstraints.optional.add(new MediaConstraints.KeyValuePair("googEchoCancellation",
                audioConstraintsMap.hasKey("echoCancellation")
                        ? ReactBridgeUtil.getMapStrValue(audioConstraintsMap, "echoCancellation")
                        : "true"));
        peerConstraints.optional.add(new MediaConstraints.KeyValuePair("googHighpassFilter",
                audioConstraintsMap.hasKey("highpassFilter")
                        ? ReactBridgeUtil.getMapStrValue(audioConstraintsMap, "highpassFilter")
                        : "true"));

        // PeerConnectionFactory.createAudioSource will throw an error when mandatory constraints contain nulls.
        // so, let's check for nulls
        checkMandatoryConstraints(peerConstraints);

        AudioSource audioSource = pcFactory.createAudioSource(peerConstraints);
        AudioTrack track = pcFactory.createAudioTrack(id, audioSource);

        // surfaceTextureHelper is initialized for videoTrack only, so its null here.
        tracks.put(id, new TrackPrivate(track, audioSource, /* videoCapturer */ null, /* surfaceTextureHelper */ null));

        return track;
    }

    private void checkMandatoryConstraints(MediaConstraints peerConstraints) {
        ArrayList<MediaConstraints.KeyValuePair> valid = new ArrayList<>(peerConstraints.mandatory.size());

        for (MediaConstraints.KeyValuePair constraint : peerConstraints.mandatory) {
            if (constraint.getValue() != null) {
                valid.add(constraint);
            } else {
                Log.d(TAG, String.format("constraint %s is null, ignoring it", constraint.getKey()));
            }
        }

        peerConstraints.mandatory.clear();
        peerConstraints.mandatory.addAll(valid);
    }

    private CameraEnumerator getCameraEnumerator() {
        if (cameraEnumerator == null) {
            if (Camera2Enumerator.isSupported(reactContext)) {
                Log.d(TAG, "Creating camera enumerator using the Camera2 API");
                cameraEnumerator = new Camera2Enumerator(reactContext);
            } else {
                Log.d(TAG, "Creating camera enumerator using the Camera1 API");
                cameraEnumerator = new Camera1Enumerator(false);
            }
        }

        return cameraEnumerator;
    }

    ReadableArray enumerateDevices() {
        WritableArray array = Arguments.createArray();
        String[] devices = getCameraEnumerator().getDeviceNames();

        for (int i = 0; i < devices.length; ++i) {
            String deviceName = devices[i];
            boolean isFrontFacing;
            try {
                // This can throw an exception when using the Camera 1 API.
                isFrontFacing = getCameraEnumerator().isFrontFacing(deviceName);
            } catch (Exception e) {
                Log.e(TAG, "Failed to check the facing mode of camera");
                continue;
            }
            WritableMap params = Arguments.createMap();
            params.putString("facing", isFrontFacing ? "front" : "environment");
            params.putString("deviceId", "" + i);
            params.putString("groupId", "");
            params.putString("label", deviceName);
            params.putString("kind", "videoinput");
            array.pushMap(params);
        }

        WritableMap audio = Arguments.createMap();
        audio.putString("deviceId", "audio-1");
        audio.putString("groupId", "");
        audio.putString("label", "Audio");
        audio.putString("kind", "audioinput");
        array.pushMap(audio);

        return array;
    }

    MediaStreamTrack getTrack(String id) {
        TrackPrivate private_ = tracks.get(id);

        return private_ == null ? null : private_.track;
    }

    /**
     * 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 map.
     */
    void getUserMedia(final ReadableMap constraints, final Callback successCallback, final Callback errorCallback) {
        AudioTrack audioTrack = null;
        VideoTrack videoTrack = null;

        if (constraints.hasKey("audio")) {
            audioTrack = createAudioTrack(constraints);
        }

        if (constraints.hasKey("video")) {
            ReadableMap videoConstraintsMap = constraints.getMap("video");

            Log.d(TAG, "getUserMedia(video): " + videoConstraintsMap);

            Activity currentActivity = reactContext.getCurrentActivity();

            if (currentActivity == null) {
                // Fail with DOMException with name InvalidStateError as per:
                // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia
                errorCallback.invoke("DOMException", "InvalidStateError");
                return;
            }

            CameraCaptureController cameraCaptureController =
                    new CameraCaptureController(currentActivity, getCameraEnumerator(), videoConstraintsMap);

            videoTrack = createVideoTrack(cameraCaptureController);
        }

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

        createStream(new MediaStreamTrack[] {audioTrack, videoTrack}, (streamId, tracksInfo) -> {
            WritableArray tracksInfoWritableArray = Arguments.createArray();

            for (WritableMap trackInfo : tracksInfo) {
                tracksInfoWritableArray.pushMap(trackInfo);
            }

            successCallback.invoke(streamId, tracksInfoWritableArray);
        });
    }

    void mediaStreamTrackSetEnabled(String trackId, final boolean enabled) {
        TrackPrivate track = tracks.get(trackId);
        if (track != null && track.videoCaptureController != null) {
            if (enabled) {
                track.videoCaptureController.startCapture();
            } else {
                if (!track.isClone()) {
                    track.videoCaptureController.stopCapture();
                }
            }
        }
    }

    void disposeAllTracks() {
        for (Map.Entry<String, TrackPrivate> entry : tracks.entrySet()) {
            try {
                entry.getValue().dispose();
            } catch (Exception e) {
                Log.w(TAG, "disposeAllTracks: error disposing " + entry.getKey(), e);
            }
        }
        tracks.clear();
    }

    void disposeTrack(String id) {
        TrackPrivate track = tracks.remove(id);
        if (track != null) {
            track.dispose();
        }
    }

    void applyConstraints(String trackId, ReadableMap constraints, Promise promise) {
        TrackPrivate track = tracks.get(trackId);
        if (track != null && track.videoCaptureController instanceof AbstractVideoCaptureController) {
            AbstractVideoCaptureController captureController =
                    (AbstractVideoCaptureController) track.videoCaptureController;
            captureController.applyConstraints(constraints, new Consumer<Exception>() {
                public void accept(Exception e) {
                    if (e != null) {
                        promise.reject(e);
                        return;
                    }

                    promise.resolve(captureController.getSettings());
                }
            });
        } else {
            promise.reject(new Exception("Camera track not found!"));
        }
    }

    void initializeConstraints(ReadableMap constraints) {
        // Handle the incoming params

        ReadableMap androidConstraints = null;
        if (constraints.hasKey("android") && constraints.getType("android") == ReadableType.Map) {
            androidConstraints = constraints.getMap("android");
        }

        // Default values
        boolean createConfigForDefaultDisplay = false;
        float scale = 1.0f;

        if (androidConstraints != null) {
            // MediaProjectionConfig need API level 34
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
                    && androidConstraints.hasKey("createConfigForDefaultDisplay")
                    && androidConstraints.getType("createConfigForDefaultDisplay") == ReadableType.Boolean) {
                createConfigForDefaultDisplay = androidConstraints.getBoolean("createConfigForDefaultDisplay");
            }
            if (androidConstraints.hasKey("resolutionScale")
                    && androidConstraints.getType("resolutionScale") == ReadableType.Number) {
                scale = (float) androidConstraints.getDouble("resolutionScale");
            }
        }

        this.createConfigForDefaultDisplay = createConfigForDefaultDisplay;
        // Force the value in [0, 1]
        this.resolutionScale = Math.max(0.0f, Math.min(1.0f, scale));

        Log.d(TAG,
                "initializeConstraints: createConfigForDefaultDisplay=" + this.createConfigForDefaultDisplay
                        + " resolutionScale=" + this.resolutionScale);
    }

    void getDisplayMedia(final ReadableMap constraints, Promise promise) {
        if (this.displayMediaPromise != null) {
            promise.reject(new RuntimeException("Another operation is pending."));
            return;
        }

        Activity currentActivity = this.reactContext.getCurrentActivity();
        if (currentActivity == null) {
            promise.reject(new RuntimeException("No current Activity."));
            return;
        }

        this.initializeConstraints(constraints);

        this.displayMediaPromise = promise;

        MediaProjectionManager mediaProjectionManager =
                (MediaProjectionManager) currentActivity.getApplication().getSystemService(
                        Context.MEDIA_PROJECTION_SERVICE);

        if (mediaProjectionManager != null) {
            UiThreadUtil.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (createConfigForDefaultDisplay == true) {
                        // MediaProjectionConfig need API level 34
                        // Return mediaProjection which restricts the user to capturing the default display
                        currentActivity.startActivityForResult(
                                mediaProjectionManager.createScreenCaptureIntent(
                                        MediaProjectionConfig.createConfigForDefaultDisplay()),
                                PERMISSION_REQUEST_CODE);
                    } else {
                        // Return mediaProjection which allows the user to decide which region is captured
                        currentActivity.startActivityForResult(
                                mediaProjectionManager.createScreenCaptureIntent(), PERMISSION_REQUEST_CODE);
                    }
                }
            });

        } else {
            promise.reject(new RuntimeException("MediaProjectionManager is null."));
        }
    }

    private void createScreenStream() {
        // Guards against onServiceConnected firing after invalidate() has disposed and nulled mFactory.
        if (webRTCModule.mFactory == null) {
            if (displayMediaPromise != null) {
                displayMediaPromise.reject("ERR_MODULE_DISPOSED", "WebRTCModule disposed during getDisplayMedia");
                displayMediaPromise = null;
            }
            return;
        }
        VideoTrack track = createScreenTrack();

        if (track == null) {
            displayMediaPromise.reject(new RuntimeException("ScreenTrack is null."));
        } else {
            createStream(new MediaStreamTrack[] {track}, (streamId, tracksInfo) -> {
                WritableMap data = Arguments.createMap();

                data.putString("streamId", streamId);

                if (tracksInfo.size() == 0) {
                    displayMediaPromise.reject(new RuntimeException("No ScreenTrackInfo found."));
                } else {
                    data.putMap("track", tracksInfo.get(0));
                    displayMediaPromise.resolve(data);
                }
            });
        }

        // Cleanup
        // Note: mediaProjectionPermissionResultData is intentionally NOT nulled here.
        // It is retained so it can be reused to create a MediaProjection for
        // screen share audio capture (AudioPlaybackCaptureConfiguration).
        displayMediaPromise = null;
    }

    void createStream(MediaStreamTrack[] tracks, BiConsumer<String, ArrayList<WritableMap>> successCallback) {
        String streamId = UUID.randomUUID().toString();
        MediaStream mediaStream = webRTCModule.mFactory.createLocalMediaStream(streamId);

        ArrayList<WritableMap> tracksInfo = new ArrayList<>();

        for (MediaStreamTrack track : tracks) {
            if (track == null) {
                continue;
            }

            if (track instanceof AudioTrack) {
                mediaStream.addTrack((AudioTrack) track);
            } else {
                mediaStream.addTrack((VideoTrack) track);
            }

            WritableMap trackInfo = Arguments.createMap();
            String trackId = track.id();

            trackInfo.putBoolean("enabled", track.enabled());
            trackInfo.putString("id", trackId);
            trackInfo.putString("kind", track.kind());
            trackInfo.putString("readyState", "live");
            trackInfo.putBoolean("remote", false);

            if (track instanceof VideoTrack) {
                TrackPrivate tp = this.tracks.get(trackId);
                AbstractVideoCaptureController vcc = tp.videoCaptureController;
                trackInfo.putMap("settings", vcc.getSettings());
            }

            if (track instanceof AudioTrack) {
                WritableMap settings = Arguments.createMap();
                settings.putString("deviceId", "audio-1");
                settings.putString("groupId", "");
                trackInfo.putMap("settings", settings);
            }

            tracksInfo.add(trackInfo);
        }

        Log.d(TAG, "MediaStream id: " + streamId);
        webRTCModule.localStreams.put(streamId, mediaStream);

        successCallback.accept(streamId, tracksInfo);
    }

    private VideoTrack createScreenTrack() {
        DisplayMetrics displayMetrics = DisplayUtils.getDisplayMetrics(reactContext.getCurrentActivity());
        int width = displayMetrics.widthPixels;
        int height = displayMetrics.heightPixels;
        ScreenCaptureController screenCaptureController = new ScreenCaptureController(
                reactContext.getCurrentActivity(), width, height, mediaProjectionPermissionResultData, resolutionScale);
        return createVideoTrack(screenCaptureController);
    }

    VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureController) {
        videoCaptureController.initializeVideoCapturer();

        VideoCapturer videoCapturer = videoCaptureController.videoCapturer;
        if (videoCapturer == null) {
            return null;
        }

        PeerConnectionFactory pcFactory = webRTCModule.mFactory;
        EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
        SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglContext);

        if (surfaceTextureHelper == null) {
            Log.d(TAG, "Error creating SurfaceTextureHelper");
            return null;
        }

        String id = UUID.randomUUID().toString();

        TrackCapturerEventsEmitter eventsEmitter = new TrackCapturerEventsEmitter(webRTCModule, id);
        videoCaptureController.setCapturerEventsListener(eventsEmitter);

        VideoSource videoSource = pcFactory.createVideoSource(videoCapturer.isScreencast());
        videoCapturer.initialize(surfaceTextureHelper, reactContext, videoSource.getCapturerObserver());

        VideoTrack track = pcFactory.createVideoTrack(id, videoSource);

        // Add dimension detection for local video tracks immediately when created
        VideoTrackAdapter localTrackAdapter = new VideoTrackAdapter(webRTCModule, -1); // Use -1 for local tracks
        localTrackAdapter.addDimensionDetector(track);

        track.setEnabled(true);
        tracks.put(id,
                new TrackPrivate(track, videoSource, videoCaptureController, surfaceTextureHelper, localTrackAdapter));

        videoCaptureController.startCapture();

        return track;
    }

    MediaStreamTrack cloneTrack(String trackId) {
        TrackPrivate track = tracks.get(trackId);
        if (track == null) {
            throw new IllegalArgumentException("No track found for id: " + trackId);
        }

        PeerConnectionFactory pcFactory = webRTCModule.mFactory;

        String id = UUID.randomUUID().toString();
        MediaStreamTrack nativeTrack = track.track;
        final MediaStreamTrack clonedNativeTrack;
        VideoTrackAdapter clonedVideoTrackAdapter = null;

        if (nativeTrack instanceof VideoTrack) {
            clonedNativeTrack = pcFactory.createVideoTrack(id, (VideoSource) track.mediaSource);

            // Create dimension detection for cloned video tracks
            clonedVideoTrackAdapter = new VideoTrackAdapter(webRTCModule, -1);
            clonedVideoTrackAdapter.addDimensionDetector((VideoTrack) clonedNativeTrack);
        } else {
            clonedNativeTrack = pcFactory.createAudioTrack(id, (AudioSource) track.mediaSource);
        }
        clonedNativeTrack.setEnabled(nativeTrack.enabled());

        final TrackPrivate clone = new TrackPrivate(clonedNativeTrack,
                track.mediaSource,
                track.videoCaptureController,
                track.surfaceTextureHelper,
                clonedVideoTrackAdapter);
        clone.setParent(track);
        tracks.put(id, clone);

        return clonedNativeTrack;
    }

    /**
     * Set video effects to the TrackPrivate corresponding to the trackId with the help of VideoEffectProcessor
     * corresponding to the names.
     * @param trackId TrackPrivate id
     * @param names VideoEffectProcessor names
     */
    void setVideoEffects(String trackId, ReadableArray names) {
        TrackPrivate track = tracks.get(trackId);

        if (track != null && track.videoCaptureController instanceof CameraCaptureController) {
            VideoSource videoSource = (VideoSource) track.mediaSource;
            SurfaceTextureHelper surfaceTextureHelper = track.surfaceTextureHelper;

            // Swap first, dispose last — otherwise a frame in flight could hit a freed
            // processor. onCapturerStopped can't replace this; it also fires on pauses.
            VideoEffectProcessor previousProcessor = track.videoEffectProcessor;
            track.videoEffectProcessor = null;

            if (names != null) {
                List<VideoFrameProcessor> processors =
                        names.toArrayList()
                                .stream()
                                .filter(name -> name instanceof String)
                                .map(name -> {
                                    VideoFrameProcessor videoFrameProcessor =
                                            ProcessorProvider.getProcessor((String) name);
                                    if (videoFrameProcessor == null) {
                                        Log.e(TAG, "no videoFrameProcessor associated with this name: " + name);
                                    }
                                    return videoFrameProcessor;
                                })
                                .filter(Objects::nonNull)
                                .collect(Collectors.toList());

                VideoEffectProcessor videoEffectProcessor = new VideoEffectProcessor(processors, surfaceTextureHelper);
                videoSource.setVideoProcessor(videoEffectProcessor);
                track.videoEffectProcessor = videoEffectProcessor;

            } else {
                videoSource.setVideoProcessor(null);
            }

            if (previousProcessor != null) {
                previousProcessor.dispose();
            }
        }
    }

    void registerTrack(AudioTrack track, AudioSource source) {
        tracks.put(track.id(), new TrackPrivate(track, source, null, null));
    }

    void registerTrack(VideoTrack track, VideoSource source, AbstractVideoCaptureController controller,
            SurfaceTextureHelper surfaceTextureHelper) {
        tracks.put(track.id(), new TrackPrivate(track, source, controller, surfaceTextureHelper));
    }

    /**
     * Application/library-specific private members of local
     * {@code MediaStreamTrack}s created by {@code GetUserMediaImpl}.
     */
    private static class TrackPrivate {
        /**
         * The {@code MediaSource} from which {@link #track} was created.
         */
        public final MediaSource mediaSource;

        public final MediaStreamTrack track;

        /**
         * The {@code VideoCapturer} from which {@link #mediaSource} was created
         * if {@link #track} is a {@link VideoTrack}.
         */
        public final AbstractVideoCaptureController videoCaptureController;

        private final SurfaceTextureHelper surfaceTextureHelper;

        /**
         * The {@code VideoTrackAdapter} for dimension detection if {@link #track} is a {@link VideoTrack}.
         */
        public final VideoTrackAdapter videoTrackAdapter;

        /** Current effect processor, disposed on filter switch and on track teardown. */
        public VideoEffectProcessor videoEffectProcessor;

        /**
         * Whether this object has been disposed or not.
         */
        private boolean disposed;

        /**
         * Whether this object is a clone of another object.
         */
        private TrackPrivate parent = null;

        /**
         * Initializes a new {@code TrackPrivate} instance.
         *
         * @param track
         * @param mediaSource            the {@code MediaSource} from which the specified
         *                               {@code code} was created
         * @param videoCaptureController the {@code AbstractVideoCaptureController} from which the
         *                               specified {@code mediaSource} was created if the specified
         *                               {@code track} is a {@link VideoTrack}
         * @param surfaceTextureHelper   the {@code SurfaceTextureHelper} for video rendering
         * @param videoTrackAdapter      the {@code VideoTrackAdapter} for dimension detection if video track
         */
        public TrackPrivate(MediaStreamTrack track, MediaSource mediaSource,
                AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper,
                VideoTrackAdapter videoTrackAdapter) {
            this.track = track;
            this.mediaSource = mediaSource;
            this.videoCaptureController = videoCaptureController;
            this.surfaceTextureHelper = surfaceTextureHelper;
            this.videoTrackAdapter = videoTrackAdapter;
            this.disposed = false;
        }

        /**
         * Backwards compatibility constructor for audio tracks
         */
        public TrackPrivate(MediaStreamTrack track, MediaSource mediaSource,
                AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper) {
            this(track, mediaSource, videoCaptureController, surfaceTextureHelper, null);
        }

        public void dispose() {
            final boolean isClone = this.isClone();
            if (!disposed) {
                if (!isClone && videoCaptureController != null) {
                    if (videoCaptureController.stopCapture()) {
                        videoCaptureController.dispose();
                    }
                }

                // After stopCapture so no frame can still reach it; before
                // surfaceTextureHelper dispose so GL is still alive for cleanup.
                if (!isClone && videoEffectProcessor != null) {
                    videoEffectProcessor.dispose();
                    videoEffectProcessor = null;
                }

                // Clean up VideoTrackAdapter for video tracks (each TrackPrivate, incl. clones, has its own)
                if (videoTrackAdapter != null && track instanceof VideoTrack) {
                    videoTrackAdapter.removeDimensionDetector((VideoTrack) track);
                    videoTrackAdapter.dispose();
                }

                /*
                 * As per webrtc library documentation - The caller still has ownership of {@code
                 * surfaceTextureHelper} and is responsible for making sure surfaceTextureHelper.dispose() is
                 * called. This also means that the caller can reuse the SurfaceTextureHelper to initialize a new
                 * VideoCapturer once the previous VideoCapturer has been disposed. */

                if (!isClone && surfaceTextureHelper != null) {
                    surfaceTextureHelper.stopListening();
                    surfaceTextureHelper.dispose();
                }

                // clones should not dispose the mediaSource as that will affect the original track
                // and other clones as well (since they share the same mediaSource).
                if (!isClone) {
                    mediaSource.dispose();
                }
                track.dispose();
                disposed = true;
            }
        }

        public void setParent(TrackPrivate parent) {
            this.parent = parent;
        }

        public boolean isClone() {
            return this.parent != null;
        }
    }

    public interface BiConsumer<T, U> {
        void accept(T t, U u);
    }
}
