<script lang="ts">
	import { onDestroy } from 'svelte';
	import Composite from './Composite.svelte';
	import FallBack from './FallBack.svelte';
	import components from '$lib/core/componentLoader';
	import services from '$lib/core/servicesLoader';
	import { dataStore } from '$lib/core/dataLoader';
	import { createCompositeStore, getCompositeStore } from '$lib/core/compositeStores';
	import { coreServices } from './coreServices';
	import { Machine, interpret } from 'xstate';

	interface ICompositeLayout {
		areas: string;
		columns?: string;
		rows?: string;
		gap?: string;
		tailwindClasses?: string;
	}

	interface IComposite {
		layout?: ICompositeLayout;
		id: string;
		slot?: string;
		component?: string;
		services?: string[];
		map?: Record<string, string>;
		store?: Record<string, any>;
		children?: IComposite[];
		servicesLoaded?: boolean;
		machine?: any;
	}

	export let composite: IComposite;
	let loadedServices: Record<string, any> = {
		core: coreServices
	};
	let layoutStyle = '';
	let machineService;

	$: {
		layoutStyle = computeLayoutStyle(composite?.layout);

		initializeAndLoadServices(composite);
		initializeCompositeState(composite);
		mapAndSubscribe(composite);

		if (composite?.children) {
			composite.children.forEach((child) => {
				initializeCompositeState(child);
				initializeAndLoadServices(child);
				mapAndSubscribe(child);
			});
		}
		if (composite?.machine) {
			const machine = Machine(composite.machine);
			machineService = interpret(machine).onTransition((state) => {
				getCompositeStore(composite.id).update((storeValue) => ({
					...storeValue,
					machine: { state: state.value }
				}));
			});
			machineService.start();
			loadedServices.machine = machineService;
		}

		if (composite?.children) {
			composite.children.forEach((child) => {
				if (child.machine) {
					const childMachine = Machine(child.machine);
					machineService = interpret(childMachine).onTransition((state) => {
						getCompositeStore(child.id).update((storeValue) => ({
							...storeValue,
							machine: { state: state.value }
						}));
					});
					machineService.start();
					loadedServices.machine = machineService;
				}
			});
		}
	}

	function computeLayoutStyle(layout?: ICompositeLayout): string {
		if (!layout) return '';
		return `
            grid-template-areas: ${layout.areas};
            ${layout.gap ? `gap: ${layout.gap};` : ''}
            ${layout.columns ? `grid-template-columns: ${layout.columns};` : ''}
            ${layout.rows ? `grid-template-rows: ${layout.rows};` : ''}
        `;
	}

	function initializeAndLoadServices(component: IComposite) {
		if (!component) return;

		if (component.services) {
			let servicePromises = component.services.map((serviceName) =>
				loadedServices[serviceName]
					? Promise.resolve(loadedServices[serviceName])
					: loadService(serviceName)
			);

			Promise.all(servicePromises).then((loaded) => {
				loaded.forEach((service, index) => {
					loadedServices[component.services[index]] = service;
				});
				component.servicesLoaded = true;
			});
		} else {
			component.servicesLoaded = true;
		}
	}

	function initializeCompositeState(child: IComposite) {
		if (child.id) {
			child.store = createCompositeStore(child.id, child.store || {});
		}

		if (child.children) {
			child.children.forEach(initializeCompositeState);
		}
	}

	let unsubscribers = [];

	function mapAndSubscribe(component: IComposite) {
		if (component.map) {
			const localStore = getCompositeStore(component.id);

			for (const [localKey, mapping] of Object.entries(component.map)) {
				const isDataMapping = mapping.startsWith('@data:');
				const isStoreMapping = mapping.startsWith('@');

				if (isDataMapping) {
					const externalKey = mapping.replace('@data:', '').trim();

					const unsubscribe = dataStore.subscribe((store) => {
						if (externalKey in store) {
							if (store[externalKey] && typeof store[externalKey].subscribe === 'function') {
								let innerUnsub = store[externalKey].subscribe((value) => {
									localStore.update((storeValue) => ({ ...storeValue, [localKey]: value }));
								});
								unsubscribers.push(innerUnsub);
							} else {
								localStore.update((storeValue) => ({
									...storeValue,
									[localKey]: store[externalKey]
								}));
							}
						}
					});

					unsubscribers.push(unsubscribe);
				} else if (isStoreMapping) {
					const [externalID, externalKey] = mapping
						.replace('@', '')
						.split(':')
						.map((item) => item.trim());

					const externalStore = getCompositeStore(externalID);
					if (externalStore) {
						const unsubscribe = externalStore.subscribe((externalState) => {
							if (externalState && externalKey in externalState) {
								localStore.update((storeValue) => ({
									...storeValue,
									[localKey]: externalState[externalKey]
								}));
							}
						});

						unsubscribers.push(unsubscribe);
					}
				}
			}
		}

		if (component.children) {
			component.children.forEach(mapAndSubscribe);
		}
	}

	onDestroy(() => {
		unsubscribers.forEach((unsub) => unsub());
	});

	async function getComponent(componentName: string) {
		if (components[componentName]) {
			const module = await components[componentName]();
			return module.default;
		}
		return FallBack;
	}

	async function loadService(serviceName: string) {
		if (services[serviceName]) {
			const module = await services[serviceName]();
			return module.default || module;
		}
		return null;
	}

	async function loadComponentAndService(component: IComposite) {
		const componentName = component.component || 'FallBack';
		return await getComponent(componentName);
	}
</script>

<div
	class={`grid w-full h-full overflow-hidden ${composite?.layout?.tailwindClasses || ''}`}
	style={layoutStyle}
>
	{#if composite?.servicesLoaded}
		{#await loadComponentAndService(composite) then Component}
			<svelte:component
				this={Component}
				id={composite.id}
				store={getCompositeStore(composite.id)}
				machine={composite.machine}
				services={loadedServices}
			/>
		{/await}
	{/if}
	{#if composite?.children}
		{#each composite.children as child (child.id)}
			<div
				class="grid w-full h-full overflow-hidden ${composite?.layout?.tailwindClasses || ''}"
				style={`grid-area: ${child.slot}`}
			>
				{#if child.servicesLoaded}
					{#await loadComponentAndService(child) then ChildComponent}
						<svelte:component
							this={ChildComponent}
							id={child.id}
							store={getCompositeStore(child.id)}
							machine={child.machine}
							services={loadedServices}
						/>
						{#if child.children && child.children.length}
							<Composite composite={child} />
						{/if}
					{/await}
				{/if}
			</div>
		{/each}
	{/if}
</div>
