import {
	type ApiData,
	type BlobRef,
	type Config,
	type EndpointInfo,
	type JsApiData,
	type DataType,
	Command,
	type Dependency,
	type ComponentMeta
} from "../types";
import { FileData } from "../upload";

const is_node =
	typeof process !== "undefined" && process.versions && process.versions.node;

export function update_object(
	object: { [x: string]: any },
	newValue: any,
	stack: (string | number)[]
): void {
	while (stack.length > 1) {
		const key = stack.shift();
		if (typeof key === "string" || typeof key === "number") {
			object = object[key];
		} else {
			throw new Error("Invalid key type");
		}
	}

	const key = stack.shift();
	if (typeof key === "string" || typeof key === "number") {
		object[key] = newValue;
	} else {
		throw new Error("Invalid key type");
	}
}

export async function walk_and_store_blobs(
	data: DataType,
	type: string | undefined = undefined,
	path: string[] = [],
	root = false,
	endpoint_info: EndpointInfo<ApiData | JsApiData> | undefined = undefined
): Promise<BlobRef[]> {
	if (Array.isArray(data)) {
		let blob_refs: BlobRef[] = [];

		await Promise.all(
			data.map(async (_, index) => {
				let new_path = path.slice();
				new_path.push(String(index));

				const array_refs = await walk_and_store_blobs(
					data[index],
					root
						? endpoint_info?.parameters[index]?.component || undefined
						: type,
					new_path,
					false,
					endpoint_info
				);

				blob_refs = blob_refs.concat(array_refs);
			})
		);

		return blob_refs;
	} else if (
		(globalThis.Buffer && data instanceof globalThis.Buffer) ||
		data instanceof Blob
	) {
		return [
			{
				path: path,
				blob: new Blob([data as any]),
				type
			}
		];
	} else if (typeof data === "object" && data !== null) {
		let blob_refs: BlobRef[] = [];
		for (const key of Object.keys(data) as (keyof typeof data)[]) {
			const new_path = [...path, key];
			const value = data[key];

			blob_refs = blob_refs.concat(
				await walk_and_store_blobs(
					value,
					undefined,
					new_path,
					false,
					endpoint_info
				)
			);
		}

		return blob_refs;
	}

	return [];
}

export function skip_queue(id: number, config: Config): boolean {
	let fn_queue = config?.dependencies?.find((dep) => dep.id == id)?.queue;
	if (fn_queue != null) {
		return !fn_queue;
	}
	return !config.enable_queue;
}

// todo: add jsdoc for this function

export function post_message<Res = any>(
	message: any,
	origin: string
): Promise<Res> {
	return new Promise((res, _rej) => {
		const channel = new MessageChannel();
		channel.port1.onmessage = (({ data }) => {
			channel.port1.close();
			res(data);
		}) as (ev: MessageEvent<Res>) => void;
		window.parent.postMessage(message, origin, [channel.port2]);
	});
}

export function handle_file(
	file_or_url: File | string | Blob | Buffer
): FileData | Blob | Command {
	if (typeof file_or_url === "string") {
		if (
			file_or_url.startsWith("http://") ||
			file_or_url.startsWith("https://")
		) {
			return {
				path: file_or_url,
				url: file_or_url,
				orig_name: file_or_url.split("/").pop() ?? "unknown",
				meta: { _type: "gradio.FileData" }
			};
		}

		if (is_node) {
			// Handle local file paths
			return new Command("upload_file", {
				path: file_or_url,
				name: file_or_url,
				orig_path: file_or_url
			});
		}
	} else if (typeof File !== "undefined" && file_or_url instanceof File) {
		return new Blob([file_or_url]);
	} else if (file_or_url instanceof Buffer) {
		return new Blob([file_or_url as any]);
	} else if (file_or_url instanceof Blob) {
		return file_or_url;
	}
	throw new Error(
		"Invalid input: must be a URL, File, Blob, or Buffer object."
	);
}

/**
 * Handles the payload by filtering out state inputs and returning an array of resolved payload values.
 * We send null values for state inputs to the server, but we don't want to include them in the resolved payload.
 *
 * @param resolved_payload - The resolved payload values received from the client or the server
 * @param dependency - The dependency object.
 * @param components - The array of component metadata.
 * @param with_null_state - Optional. Specifies whether to include null values for state inputs. Default is false.
 * @returns An array of resolved payload values, filtered based on the dependency and component metadata.
 */
export function handle_payload(
	resolved_payload: unknown[],
	dependency: Dependency,
	components: ComponentMeta[],
	type: "input" | "output",
	with_null_state = false
): unknown[] {
	if (type === "input" && !with_null_state) {
		throw new Error("Invalid code path. Cannot skip state inputs for input.");
	}
	// data comes from the server with null state values so we skip
	if (type === "output" && with_null_state) {
		return resolved_payload;
	}

	let updated_payload: unknown[] = [];
	let payload_index = 0;
	const deps = type === "input" ? dependency.inputs : dependency.outputs;
	for (let i = 0; i < deps.length; i++) {
		const input_id = deps[i];
		const component = components.find((c) => c.id === input_id);

		if (component?.type === "state") {
			// input + with_null_state needs us to fill state with null values
			if (with_null_state) {
				if (resolved_payload.length === deps.length) {
					const value = resolved_payload[payload_index];
					updated_payload.push(value);
					payload_index++;
				} else {
					updated_payload.push(null);
				}
			} else {
				// this is output & !with_null_state, we skip state inputs
				// the server payload always comes with null state values so we move along the payload index
				payload_index++;
				continue;
			}
			// input & !with_null_state isn't a case we care about, server needs null
			continue;
		} else {
			const value = resolved_payload[payload_index];
			updated_payload.push(value);
			payload_index++;
		}
	}

	return updated_payload;
}
