<script lang="ts" module>
	export class PixelControl {
		private _pixelID: string;
		private _testEventCode?: string = undefined;
		private _trackingManager: MaybeGetter<TrackingManager | undefined>;

		private static _baseLoaded: boolean = false;

		static get baseLoaded(): boolean {
			return this._baseLoaded;
		}

		/**
		 * Ensures that the Meta Pixel base has been loaded before
		 * allowing further operations.
		 * @throws Error if the Meta Pixel API is not loaded.
		 */
		static loadGuard(): void {
			if (!this._baseLoaded || !window._fbq) {
				throw new Error('Meta Pixel API has not been loaded. Call PixelControl.load() first.');
			}
		}

		private constructor(
			trackingManager: MaybeGetter<TrackingManager | undefined>,
			pixelID: string,
			testEventCode?: string
		) {
			this._trackingManager = trackingManager;
			this._pixelID = pixelID;
			this._testEventCode = testEventCode;
		}

		/** Loads the Meta Pixel base script. */
		static load() {
			if (this._baseLoaded && window._fbq) return;
			if (!window._fbq) {
				PixelControl.revokeConsent(); // Initialize without consent
				loadMetaPixel(); // Load the Meta Pixel script
			}
			this._baseLoaded = true;
		}

		/** Tells the Meta pixel that the user has given consent for tracking. */
		static grantConsent() {
			this.loadGuard();
			window.fbq?.('consent', 'grant');
		}

		/** Tells the Meta pixel that the user has revoked consent for tracking. */
		static revokeConsent() {
			this.loadGuard();
			window.fbq?.('consent', 'revoke');
		}

		/**
		 * Returns a PixelControl instance for the given Meta Pixel ID. If
		 * the base Meta Pixel script has not been loaded yet, it will be
		 * loaded automatically. Optionally sets a test event code for the Pixel.
		 * Does NOT initialize the Pixel; call `fireInit()` on the returned instance
		 * before tracking events.
		 * @param trackingManager Tracking manager to handle user consent for tracking
		 * @param pixelID Meta Pixel ID
		 * @param options Optional settings
		 * @returns PixelControl instance
		 */
		static for(
			trackingManager: MaybeGetter<TrackingManager | undefined>,
			pixelID: string,
			options?: {
				/**
				 * if provided, events fired will always have this code attached
				 * to prevent them from polluting real analytics data.
				 */
				testEventCode?: string;
			}
		): PixelControl {
			PixelControl.load();
			window.fbq('init', pixelID);
			return new PixelControl(trackingManager, pixelID, options?.testEventCode);
		}

		/**
		 * Initializes this pixel with the Meta Pixel API including any advanced
		 * matching data and options.
		 * @param advancedMatching Advanced matching data
		 * @param initOptions Initialization options
		 * @returns this PixelControl instance
		 */
		fireInit(advancedMatching?: AdvancedMatching, initOptions?: InitOptions): PixelControl {
			PixelControl.loadGuard();
			window.fbq('init', this._pixelID, advancedMatching, initOptions);
			return this;
		}

		/**
		 * Checks if the Meta Pixel has consent to track user data
		 * and if the Pixel has been loaded.
		 * @returns true if tracking is allowed, false otherwise.
		 * @throws Error if the Meta Pixel is not loaded.
		 */
		consentGuard(): boolean {
			PixelControl.loadGuard();
			const trackingManager = resolveGetter(this._trackingManager);
			return trackingManager?.haveUserConsent() ?? false;
		}

		/**
		 * Sends a PageView event
		 * @throws Error if the Meta Pixel is not initialized.
		 */
		pageView() {
			if (!this.consentGuard()) return;
			window.fbq('track', 'PageView', undefined, { test_event_code: this._testEventCode });
		}

		/**
		 * Tracks a standard event for this pixel (uses `trackSingle` under the hood)
		 * @throws Error if the Meta Pixel is not initialized.
		 */
		track<K extends StandardEventName>(event: K, params?: EventParamsByName[K], eventID?: string) {
			if (!this.consentGuard()) return;
			window.fbq('trackSingle', this._pixelID, event, params, {
				eventID,
				test_event_code: this._testEventCode
			});
		}

		/**
		 * Tracks a custom event for this pixel (uses `trackSingleCustom` under the hood)
		 * @throws Error if the Meta Pixel is not initialized.
		 */
		trackCustom(event: string, params?: CommonParams & CustomParams, eventID?: string) {
			if (!this.consentGuard()) return;
			window.fbq('trackSingleCustom', this._pixelID, event, params, {
				eventID,
				test_event_code: this._testEventCode
			});
		}
	}
</script>

<script lang="ts">
	import { onMount } from 'svelte';

	import type { TrackingManager } from './tracking.svelte.ts';
	import type {
		EventParamsByName,
		StandardEventName,
		CommonParams,
		CustomParams,
		AdvancedMatching,
		InitOptions
	} from './types/fbq.js';
	import { loadMetaPixel } from './util/meta-pixel-loader.ts';
	import { onNavigate } from '$app/navigation';
	import { resolveGetter, type MaybeGetter } from './util/getter.ts';

	interface Props {
		/** Meta Pixel ID */
		pixelID: string;

		/**
		 * If a test event code is available, events fired will always have this
		 * code attached to prevent them from polluting real analytics data.
		 */
		testEventCode?: string;

		/**
		 * Controls whether page views are automatically tracked by this
		 * component (default: true).
		 */
		autoPageView?: boolean;

		/**
		 * Tracking manager to handle user consent for tracking. If omitted
		 * tracking is disabled by default until consent is granted via
		 * PixelControl.grantConsent().
		 */
		trackingManager?: TrackingManager;
	}

	let { pixelID, testEventCode, autoPageView = true, trackingManager }: Props = $props();

	let pixel = $state<PixelControl | null>(null);

	onMount(() => {
		if (!trackingManager) {
			throw new Error('MetaPixel component requires a TrackingManager to manage consent.');
		}
		PixelControl.load();
		pixel = PixelControl.for(trackingManager, pixelID, { testEventCode }).fireInit();

		trackingManager.runWithConsent(() => {
			if (autoPageView && pixel) {
				pixel.pageView();
			}
		});
	});

	onNavigate(() => {
		trackingManager?.runWithConsent(() => {
			if (autoPageView && pixel) {
				pixel.pageView();
			}
		});
	});
</script>
