import { ApolloClient, useApolloClient } from "@apollo/client";
import { useIsFocused } from "@react-navigation/native";
import * as gql from "railbird-gql";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
import * as RNFS from "react-native-fs";
import {
	Camera,
	CameraRuntimeError,
	Orientation,
	PhotoFile,
	VideoFile,
	useCameraDevice,
	useCameraFormat,
	useCameraPermission,
} from "react-native-vision-camera";
import { RecordingButton } from "./capture-button";
import { useIsForeground } from "./is-foreground";

type Dictionary<KeyType extends string | number | symbol, ValueType> = {
	[key in KeyType]: ValueType;
};

class StreamUploadManager<TCacheShape> {
	client: ApolloClient<TCacheShape>;
	videoId: number;
	nextUploadIdToRequest: number = 0;
	highestUploadLinkObtained: number = -1;
	prefetchedUploadLinks: Dictionary<number, string> = {};
	uploadQueue: Array<() => Promise<void>> = [];
	isUploading: boolean = false;

	constructor(client: ApolloClient<TCacheShape>, videoId: number) {
		this.client = client;
		this.videoId = videoId;
	}

	enqueueUploadTask(task: () => Promise<void>) {
		this.uploadQueue.push(task);
		this.processUploadQueue();
	}

	async processUploadQueue() {
		if (this.isUploading || this.uploadQueue.length === 0) {
			return;
		}
		this.isUploading = true;
		const task = this.uploadQueue.shift();
		try {
			if (task) await task();
		} catch (error) {
			console.error("Error processing upload task", error);
		} finally {
			this.isUploading = false;
			this.processUploadQueue();
		}
	}

	async uploadChunk({ filepath, index }: { filepath: string; index: number }) {
		this.enqueueUploadTask(async () => {
			const uploadUrl = await this.getUploadLink(index);
			const uploadRequest = RNFS.uploadFiles({
				toUrl: uploadUrl,
				files: [{ filepath: filepath }],
				method: "PUT",
				headers: {
					"Content-Type": "application/octet-stream",
				},
				begin: (res) => {
					console.log("Start upload", res);
				},
				progress: (res) => {
					console.log("Uploading", res);
				},
			});
			console.log(JSON.stringify(uploadRequest));
			const result = await uploadRequest.promise;
			if (result.statusCode === 200) {
				console.log(`${filepath} Uploaded`);
			} else {
				console.error("SERVER ERROR");
			}
		});
	}

	async getUploadLink(chunkId: number): Promise<string> {
		if (this.prefetchedUploadLinks[chunkId]) {
			return this.prefetchedUploadLinks[chunkId];
		}
		return this.requestUploadLink(chunkId);
	}

	async requestUploadLink(chunkId: number): Promise<string> {
		console.log(`Requesting upload link for chunk ${chunkId}`);
		const result = await this.client.mutate({
			mutation: gql.GetUploadLinkDocument,
			variables: { videoId: this.videoId, chunkIndex: chunkId },
		});
		this.prefetchedUploadLinks[chunkId] = result.data.getUploadLink.uploadUrl;
		return result.data.getUploadLink.uploadUrl;
	}
}

export default function CameraScreen({
	route,
	navigation,
}): React.ReactElement {
	const apolloClient = useApolloClient();
	const [createUpload, { data, loading, error }] =
		gql.useCreateUploadStreamMutation();

	const [uploadManager, setUploadManager] = useState(null);

	useEffect(() => {
		if (
			data &&
			data.createUploadStream &&
			data.createUploadStream.videoId &&
			!uploadManager
		) {
			const newVideoId = data.createUploadStream.videoId;
			console.log(`VideoId: ${newVideoId}`);
			setUploadManager(new StreamUploadManager(apolloClient, newVideoId));
		}
	}, [data, uploadManager]);

	const camera = useRef<Camera>(null);
	const { hasPermission, requestPermission } = useCameraPermission();
	const [isCameraInitialized, setIsCameraInitialized] =
		useState<boolean>(false);

	const isForeground = useIsForeground();
	const isFocused = useIsFocused();
	const isActive = isForeground && isFocused;

	const onError = useCallback((error: CameraRuntimeError) => {
		console.error(error);
	}, []);

	const onInitialized = useCallback(() => {
		console.log("Camera initialized!");
		setIsCameraInitialized(true);
		createUpload({ variables: { videoName: "Test" } });
	}, []);

	const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => {
		console.log(`Media captured! ${JSON.stringify(media)}`);
	}, []);

	const onVideoChunkReady = useCallback(
		(event) => {
			console.log(
				`Chunk ready in react-native ${JSON.stringify(event.nativeEvent)}`,
			);
			uploadManager.uploadChunk(event.nativeEvent);
		},
		[uploadManager],
	);

	if (!hasPermission) {
		requestPermission();
		// Error handling in case they refuse to give permission
	}

	const device = useCameraDevice("back");
	const format = useCameraFormat(device, [
		{ videoResolution: { width: 1920, height: 1080 } },
		{ fps: 30 },
	]);

	// TODO(#60): setOrientation should be called when changes are detected
	const [orientation, setOrientation] = useState<Orientation>("portrait");

	const toggleOrientation = () => {
		setOrientation((currentOrientation) =>
			currentOrientation === "landscape-left" ? "portrait" : "landscape-left",
		);
	};

	// Replace with error handling
	if (device === null) {
		console.log(device);
		return (
			<Text>
				Camera not available. Does user have permissions: {hasPermission}
			</Text>
		);
	}
	return (
		hasPermission && (
			<View style={styles.container}>
				<Camera
					ref={camera}
					style={StyleSheet.absoluteFill}
					device={device}
					format={format}
					onInitialized={onInitialized}
					onError={onError}
					// @ts-ignore
					onVideoChunkReady={onVideoChunkReady}
					video={true}
					orientation={orientation}
					isActive={isActive}
				/>
				<View
					style={
						orientation === "portrait"
							? styles.goBackPortrait
							: styles.goBackLandscape
					}
				>
					<Button title="Go back" onPress={() => navigation.goBack()} />
				</View>
				<RecordingButton
					style={[
						styles.captureButton,
						orientation === "portrait" ? styles.portrait : styles.landscape,
					]}
					camera={camera}
					onMediaCaptured={onMediaCaptured}
					enabled={isCameraInitialized}
				/>
				<View
					style={[
						styles.button,
						orientation === "portrait"
							? styles.togglePortrait
							: styles.toggleLandscape,
					]}
				>
					<Button
						title="Toggle Orientation"
						onPress={toggleOrientation}
						color="#841584"
						accessibilityLabel="Toggle camera orientation"
					/>
				</View>
			</View>
		)
	);
}

const styles = StyleSheet.create({
	container: {
		flex: 1,
		backgroundColor: "black",
	},
	captureButton: {
		position: "absolute",
		alignSelf: "center",
	},
	button: {
		position: "absolute",
		alignSelf: "center",
	},
	togglePortrait: {
		bottom: 110, // needs refined
	},
	toggleLandscape: {
		transform: [{ rotate: "90deg" }],
		bottom: "43%", // Should come from SafeAreaProvider, hardcoded right now, should roughly appear above the button
		left: 50, // needs refined
	},
	portrait: {
		bottom: 20, // needs refined
	},
	landscape: {
		bottom: "40%", // Should come from SafeAreaProvider
		left: 20, // needs refined
	},
	goBackPortrait: {
		position: "absolute",
		top: 20, // or wherever you want the button to be positioned in portrait
		left: 20, // or wherever you want the button to be positioned in portrait
	},
	goBackLandscape: {
		position: "absolute",
		top: 40,
		right: 20,
		transform: [{ rotate: "90deg" }],
	},
});
