<!-- @component
MetaPixel integrates the Meta (Facebook) Pixel into your Svelte application,
allowing you to track page views and custom events while respecting user consent
for tracking. The component manages the lifecycle of the Meta Pixel script and
PixelControl interface.

The PixelControl class also allows you to directly manage multiple Pixel
instances and handle event tracking with optional test event codes without
using the MetaPixel component.
-->

<script lang="ts" module>
	export type PixelControlOptions = {
		/**
		 * if provided, events fired will always have this code attached
		 * to prevent them from polluting real analytics data.
		 */
		testEventCode?: string;
		/** Advanced matching data */
		advancedMatching?: AdvancedMatching;
		/** Initialization options */
		initOptions?: InitOptions;
	};

	export class PixelControl {
		private _pixelID: string;
		private _testEventCode?: string = undefined;
		private _trackingManager: MaybeGetter<TrackingManager | undefined>;

		private static _baseLoaded: boolean = false;
		private static _registeredPixels: Record<string, PixelControl> = {};

		/** Indicates whether the Meta Pixel base script has been loaded. */
		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,
			options?: PixelControlOptions
		) {
			this._trackingManager = trackingManager;
			this._pixelID = pixelID;
			this._testEventCode = options?.testEventCode;
		}

		/** Loads the Meta Pixel base script. */
		static async load() {
			if (this._baseLoaded && !!window.fbq) return;
			if (!window.fbq) {
				try {
					await loadMetaPixel(); // Load the Meta Pixel script
				} catch (e) {
					log.warn('Failed to load Meta Pixel script, all events will be queued.', e);
				}
			}
			this._baseLoaded = true;
			log.debug('Meta Pixel base script loaded.');
		}

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

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

		/**
		 * Registers 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.
		 * Should only be called once for each Pixel ID, use PixelControl.get()
		 * to retrieve existing instances.
		 * @param trackingManager Tracking manager to handle user consent for tracking
		 * @param pixelID Meta Pixel ID
		 * @param options Optional settings
		 * @returns PixelControl instance
		 */
		static initialize(
			trackingManager: MaybeGetter<TrackingManager | undefined>,
			pixelID: string,
			options?: PixelControlOptions
		): PixelControl {
			// Load the base script if not already loaded
			PixelControl.load();

			// Check for existing PixelControl instance
			if (this._registeredPixels[pixelID]) {
				log.warn(
					`PixelControl instance for Meta Pixel ID: ${pixelID} already exists. Returning existing instance.`
				);
				return this._registeredPixels[pixelID];
			}

			// Create and register the PixelControl instance
			const pixel = new PixelControl(trackingManager, pixelID, options);
			this._registeredPixels[pixelID] = pixel;

			// Fire initialization
			window.fbq('init', pixel._pixelID, options?.advancedMatching, options?.initOptions);
			log.debug(`Meta Pixel [${pixel._pixelID}] initialized.`);

			return pixel;
		}

		/**
		 * Returns an existing PixelControl instance for the given Meta Pixel ID.
		 * @param pixelID Meta Pixel ID
		 * @returns PixelControl instance
		 * @throws Error if no PixelControl instance is found for the given ID.
		 */
		static get(pixelID: string): PixelControl {
			const pixel = this._registeredPixels[pixelID];
			if (!pixel) {
				throw new Error(`No PixelControl instance found for Meta Pixel ID: ${pixelID}`);
			}
			return pixel;
		}

		/**
		 * 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 });
			log.debug(
				`Meta Pixel [${this._pixelID}] PageView event sent (test 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
			});
			log.debug(
				`Meta Pixel [${this._pixelID}] ${event} event sent (test 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
			});
			log.debug(
				`Meta Pixel [${this._pixelID}] ${event} custom event sent (test code: ${this._testEventCode}).`
			);
		}
	}
</script>

<script lang="ts">
	import { createContext, 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';
	import log from 'loglevel';

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

		/** Meta Pixel ID */
		pixelID: string;

		/** Meta Pixel Options */
		pixelOptions?: PixelControlOptions;

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

	let { pixelID, pixelOptions, 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.initialize(trackingManager, pixelID, pixelOptions);

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

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

	export const getPixelControl = (): PixelControl => {
		if (!pixel) {
			throw new Error('MetaPixel component has not been initialized yet, wait for onMount.');
		}
		return pixel;
	};
</script>
