/*
 * Kyoo - A portable and vast media library solution.
 * Copyright (c) Kyoo.
 *
 * See AUTHORS.md and LICENSE file in the project root for full license information.
 *
 * Kyoo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * Kyoo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 */

import { type Audio, type Episode, type Subtitle, getLocalSetting, useAccount } from "@kyoo/models";
import { useSnackbar } from "@kyoo/primitives";
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { useAtomCallback } from "jotai/utils";
import {
	type ElementRef,
	memo,
	useCallback,
	useEffect,
	useLayoutEffect,
	useRef,
	useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import NativeVideo, { canPlay, type VideoProps } from "./video";

export const playAtom = atom(true);
export const loadAtom = atom(false);

export enum PlayMode {
	Direct,
	Hls,
}
export const playModeAtom = atom<PlayMode>(
	getLocalSetting("playmode", "direct") !== "auto" ? PlayMode.Direct : PlayMode.Hls,
);

export const bufferedAtom = atom(0);
export const durationAtom = atom<number | undefined>(undefined);

export const progressAtom = atom(
	(get) => get(privateProgressAtom),
	(get, set, update: number | ((value: number) => number)) => {
		const run = (value: number) => {
			set(privateProgressAtom, value);
			set(publicProgressAtom, value);
		};
		if (typeof update === "function") run(update(get(privateProgressAtom)));
		else run(update);
	},
);
const privateProgressAtom = atom(0);
const publicProgressAtom = atom(0);

export const volumeAtom = atom(100);
export const mutedAtom = atom(false);

export const fullscreenAtom = atom(
	(get) => get(privateFullscreen),
	(get, set, update: boolean | ((value: boolean) => boolean)) => {
		const run = async (value: boolean) => {
			try {
				if (value) {
					await document.body.requestFullscreen({
						navigationUI: "hide",
					});
					set(privateFullscreen, true);
					// @ts-expect-error Firefox does not support this so ts complains
					await screen.orientation.lock("landscape");
				} else {
					if (document.fullscreenElement) await document.exitFullscreen();
					set(privateFullscreen, false);
					screen.orientation.unlock();
				}
			} catch (e) {
				console.error(e);
			}
		};
		if (typeof update === "function") run(update(get(privateFullscreen)));
		else run(update);
	},
);
const privateFullscreen = atom(false);

export const subtitleAtom = atom<Subtitle | null>(null);
export const audioAtom = atom<Audio>({ index: 0 } as Audio);

export const Video = memo(function Video({
	links,
	subtitles,
	audios,
	codec,
	setError,
	fonts,
	startTime: startTimeP,
	metadata,
	...props
}: {
	links?: Episode["links"];
	subtitles?: Subtitle[];
	audios?: Audio[];
	codec?: string;
	setError: (error: string | undefined) => void;
	fonts?: string[];
	startTime?: number | null;
	metadata: {
		title?: string;
		description?: string;
		imageUri?: string;
		previous?: string;
		next?: string;
	};
} & Partial<VideoProps>) {
	const ref = useRef<ElementRef<typeof NativeVideo> | null>(null);
	const [isPlaying, setPlay] = useAtom(playAtom);
	const setLoad = useSetAtom(loadAtom);
	const [source, setSource] = useState<string | null>(null);
	const [mode, setPlayMode] = useAtom(playModeAtom);

	const startTime = useRef(startTimeP);
	useLayoutEffect(() => {
		startTime.current = startTimeP;
	}, [startTimeP]);

	const publicProgress = useAtomValue(publicProgressAtom);
	const setPrivateProgress = useSetAtom(privateProgressAtom);
	const setPublicProgress = useSetAtom(publicProgressAtom);
	const setBuffered = useSetAtom(bufferedAtom);
	useEffect(() => {
		ref.current?.seek(publicProgress);
	}, [publicProgress]);

	const getProgress = useAtomCallback(useCallback((get) => get(progressAtom), []));
	useEffect(() => {
		// Reset the state when a new video is loaded.

		let newMode = getLocalSetting("playmode", "direct") !== "auto" ? PlayMode.Direct : PlayMode.Hls;
		// Only allow direct play if the device supports it
		if (newMode === PlayMode.Direct && codec && !canPlay(codec)) {
			console.log(`Browser can't natively play ${codec}, switching to hls stream.`);
			newMode = PlayMode.Hls;
		}
		setPlayMode(newMode);

		setSource((newMode === PlayMode.Direct ? links?.direct : links?.hls) ?? null);
		setLoad(true);
		setPrivateProgress(startTime.current ?? 0);
		setPublicProgress(startTime.current ?? 0);
		setPlay(true);
	}, [links, codec, setLoad, setPrivateProgress, setPublicProgress, setPlay, setPlayMode]);

	// biome-ignore lint/correctness/useExhaustiveDependencies: do not change source when links change, this is done above
	useEffect(() => {
		setSource((mode === PlayMode.Direct ? links?.direct : links?.hls) ?? null);
		// keep current time when changing between direct and hls.
		startTime.current = getProgress();
		setPlay(true);
	}, [mode, getProgress, setPlay]);

	const account = useAccount();
	const defaultSubLanguage = account?.settings.subtitleLanguage;
	const setSubtitle = useSetAtom(subtitleAtom);

	// When the video change, try to persist the subtitle language.
	// biome-ignore lint/correctness/useExhaustiveDependencies: Also include the player ref, it can be initalised after the subtitles.
	useEffect(() => {
		if (!subtitles) return;
		setSubtitle((subtitle) => {
			const subRet = subtitle
				? subtitles.find(
						(x) => x.language === subtitle.language && x.isForced === subtitle.isForced,
					)
				: null;
			if (subRet) return subRet;
			if (!defaultSubLanguage) return null;
			if (defaultSubLanguage === "default") return subtitles.find((x) => x.isDefault) ?? null;
			return subtitles.find((x) => x.language === defaultSubLanguage) ?? null;
		});
	}, [subtitles, setSubtitle, defaultSubLanguage, ref.current]);

	const defaultAudioLanguage = account?.settings.audioLanguage ?? "default";
	const setAudio = useSetAtom(audioAtom);
	// When the video change, try to persist the subtitle language.
	// biome-ignore lint/correctness/useExhaustiveDependencies: Also include the player ref, it can be initalised after the subtitles.
	useEffect(() => {
		if (!audios) return;
		setAudio((audio) => {
			if (audio) {
				const ret = audios.find((x) => x.language === audio.language);
				if (ret) return ret;
			}
			if (defaultAudioLanguage !== "default") {
				const ret = audios.find((x) => x.language === defaultAudioLanguage);
				if (ret) return ret;
			}
			return audios.find((x) => x.isDefault) ?? audios[0];
		});
	}, [audios, setAudio, defaultAudioLanguage, ref.current]);

	const volume = useAtomValue(volumeAtom);
	const isMuted = useAtomValue(mutedAtom);

	const setFullscreen = useSetAtom(privateFullscreen);
	useEffect(() => {
		if (Platform.OS !== "web") return;
		const handler = () => {
			setFullscreen(document.fullscreenElement != null);
		};
		document.addEventListener("fullscreenchange", handler);
		return () => document.removeEventListener("fullscreenchange", handler);
	});

	const createSnackbar = useSnackbar();
	const { t } = useTranslation();

	if (!source || !links) return null;
	return (
		<NativeVideo
			ref={ref}
			{...props}
			source={{
				uri: source,
				startPosition: startTime.current ? startTime.current * 1000 : undefined,
				metadata: metadata,
				...links,
			}}
			showNotificationControls
			playInBackground
			playWhenInactive
			paused={!isPlaying}
			muted={isMuted}
			volume={volume}
			resizeMode="contain"
			onBuffer={({ isBuffering }) => setLoad(isBuffering)}
			onError={(status) => {
				console.error(status);
				setError(status.error.errorString);
			}}
			onProgress={(progress) => {
				setPrivateProgress(progress.currentTime);
				setBuffered(progress.playableDuration);
			}}
			onPlaybackStateChanged={(state) => setPlay(state.isPlaying)}
			fonts={fonts}
			subtitles={subtitles}
			onMediaUnsupported={() => {
				createSnackbar({
					key: "unsuported",
					label: t("player.unsupportedError"),
					duration: 3,
				});
				if (mode === PlayMode.Direct) setPlayMode(PlayMode.Hls);
			}}
		/>
	);
});
