export const LIVE_FRAME_INIT = 0x01;
export const LIVE_FRAME_MEDIA = 0x02;
export const LIVE_FRAME_ERROR = 0x03;

export type LiveFrame = {
	type: number;
	payload: Uint8Array;
};

export function decodeLiveFrame(data: ArrayBuffer): LiveFrame | null {
	const bytes = new Uint8Array(data);
	if (bytes.length === 0) {
		return null;
	}
	return {
		type: bytes[0],
		payload: bytes.slice(1)
	};
}

export class SourceBufferAppender {
	private readonly sourceBuffer: SourceBuffer;
	private readonly onError: (err: unknown) => void;
	private readonly queue: Uint8Array[] = [];
	private disposed = false;

	constructor(sourceBuffer: SourceBuffer, onError: (err: unknown) => void) {
		this.sourceBuffer = sourceBuffer;
		this.onError = onError;
		this.onUpdateEnd = this.onUpdateEnd.bind(this);
		this.sourceBuffer.addEventListener("updateend", this.onUpdateEnd);
	}

	append(segment: Uint8Array) {
		if (this.disposed || segment.length === 0) {
			return;
		}
		this.queue.push(segment);
		this.drain();
	}

	pendingSegments(): number {
		return this.queue.length;
	}

	dispose() {
		if (this.disposed) {
			return;
		}
		this.disposed = true;
		this.queue.length = 0;
		this.sourceBuffer.removeEventListener("updateend", this.onUpdateEnd);
	}

	private onUpdateEnd() {
		this.drain();
	}

	private drain() {
		if (this.disposed || this.sourceBuffer.updating || this.queue.length === 0) {
			return;
		}
		const next = this.queue.shift();
		if (!next) {
			return;
		}
		try {
			const raw = next.buffer.slice(next.byteOffset, next.byteOffset + next.byteLength) as ArrayBuffer;
			this.sourceBuffer.appendBuffer(raw);
		} catch (err) {
			this.onError(err);
		}
	}
}

export function pickLiveMimeType(initSegment: Uint8Array): string | null {
	const hasAvc1 = includesASCII(initSegment, "avc1");
	const hasHvc1 = includesASCII(initSegment, "hvc1");
	const hasHev1 = includesASCII(initSegment, "hev1");
	const avcCodec = parseAvcCodec(initSegment);

	const candidates: string[] = [];
	if (hasAvc1 && avcCodec) {
		candidates.push(`video/mp4; codecs="${avcCodec},mp4a.40.2"`);
		candidates.push(`video/mp4; codecs="${avcCodec}"`);
	}
	if (hasAvc1) {
		candidates.push('video/mp4; codecs="avc1.640028,mp4a.40.2"');
		candidates.push('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
	}
	if (hasHvc1) {
		candidates.push('video/mp4; codecs="hvc1,mp4a.40.2"');
		candidates.push('video/mp4; codecs="hvc1"');
	}
	if (hasHev1) {
		candidates.push('video/mp4; codecs="hev1,mp4a.40.2"');
		candidates.push('video/mp4; codecs="hev1"');
	}
	candidates.push("video/mp4");

	for (const candidate of candidates) {
		if (MediaSource.isTypeSupported(candidate)) {
			return candidate;
		}
	}
	return null;
}

export function buildLiveWsUrl(streamId: string): string {
	const current = new URL(window.location.href);
	const apiBaseParam = current.searchParams.get("apiBase");
	const apiBase = apiBaseParam ? new URL(apiBaseParam, current) : current;
	const protocol = apiBase.protocol === "https:" ? "wss:" : "ws:";
	return `${protocol}//${apiBase.host}/api/cctv/live/ws/${encodeURIComponent(streamId)}`;
}

function parseAvcCodec(data: Uint8Array): string | null {
	const idx = indexOfASCII(data, "avcC");
	if (idx < 0 || idx+8 >= data.length) {
		return null;
	}
	const profile = data[idx + 5];
	const compatibility = data[idx + 6];
	const level = data[idx + 7];
	return `avc1.${hex2(profile)}${hex2(compatibility)}${hex2(level)}`;
}

function includesASCII(data: Uint8Array, text: string): boolean {
	return indexOfASCII(data, text) >= 0;
}

function indexOfASCII(data: Uint8Array, text: string): number {
	if (text.length === 0 || text.length > data.length) {
		return -1;
	}
	const codes = Array.from(text, (char) => char.charCodeAt(0));
	for (let i = 0; i <= data.length - codes.length; i++) {
		let matched = true;
		for (let j = 0; j < codes.length; j++) {
			if (data[i + j] !== codes[j]) {
				matched = false;
				break;
			}
		}
		if (matched) {
			return i;
		}
	}
	return -1;
}

function hex2(value: number): string {
	return value.toString(16).toUpperCase().padStart(2, "0");
}
