import { createEffect, createSignal, For, onMount, ParentComponent, useContext } from 'solid-js';
import OlFeature from 'ol/Feature';
import OlMap from 'ol/Map';
import OlView from 'ol/View';
import { isEmpty as isEmptyExtend } from 'ol/extent';
import { FeatureLike as OlFeatureLike } from 'ol/Feature';
import OlOSM from 'ol/source/OSM';
import OlOverlay from 'ol/Overlay';
import OlVectorSource from 'ol/source/Vector';
import { Tile as OlTileLayer, Vector as OlVectorLayer } from 'ol/layer';
import { Circle, Stroke, Style } from 'ol/style';
import { easeOut } from 'ol/easing';
import { getVectorContext } from 'ol/render';
import { unByKey } from 'ol/Observable';

import { Stop } from '../types';
import { BusinessDataContext, BusinessDataStore } from "../businessData";
import { SearchContext, SearchStore } from "./searchStore";
import { MapStop } from "./mapStop";
import { StopPopup } from "./stopPopup";


import "./map.scss";


export const Map: ParentComponent<{}> = () => {
	const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
	const searchStore: SearchStore | undefined = useContext(SearchContext);
	if (businessDataStore === undefined || searchStore === undefined)
		return <div />;

	const { getStop } = businessDataStore;
	const { getAllMapFeatures, getFoundStops, getHighlightedStop } = searchStore;

	const [selectedMapStop, setSelectedMapStop] = createSignal<Stop | undefined>(undefined);
	const [isPopupDisplayed, setPopupDisplayed] = createSignal<boolean>(false);

	const mapCenter = [260769.80336542107, 6250587.867330259]; // EPSG:3857
	const fitDurationMs = 1500;
	const flashDurationMs = 2000;
	// TODO: Set padding according to the marker design.
	const fitPointsPadding = [50, 50, 50, 50];

	let mapDiv: HTMLDivElement | undefined = undefined;
	let popup: StopPopup | undefined = undefined;

	const stopVectorSource = new OlVectorSource({ features: [] });
	const stopVectorLayer = new OlVectorLayer({ source: stopVectorSource });

	let overlay: OlOverlay | undefined = undefined;
	let map: OlMap | undefined = undefined;

	const displayedFeatures: Record<number, OlFeature> = {};

	const buildMap = (div: HTMLDivElement): void => {
		overlay = new OlOverlay({
			element: popup,
			autoPan: {
				animation: {
					duration: 250,
				},
			},
		});
		map = new OlMap({
			target: div,
			controls: [], // remove controls
			view: new OlView({
				center: mapCenter,
				zoom: 10,
			}),
			layers: [
				new OlTileLayer({
					source: new OlOSM(),
				}),
				stopVectorLayer,
			],
			overlays: [overlay],
		});
		map.on('singleclick', onClickedMap);
	}

	const onClickedMap = async (event): Promise<void> => {
		const features = await stopVectorLayer.getFeatures(event.pixel);
		// Handle only the first feature
		if (features.length > 0) {
			await onClickedFeature(features[0]);
		}
		else {
			setPopupDisplayed(false);
			setSelectedMapStop(undefined);
		}
	}

	const onClickedFeature = async (feature: OlFeatureLike): Promise<void> => {
		const stopId: number = feature.getId();
		const stop = getStop(stopId);
		// TODO: Handle StopArea (use center given by the backend)
		if (stop?.epsg3857_x !== undefined && stop?.epsg3857_y !== undefined) {
			setSelectedMapStop(stop);
			map?.getView().animate(
				{
					center: [stop.epsg3857_x, stop.epsg3857_y],
					duration: 1000
				},
				// Display the popup once the animation finished
				() => setPopupDisplayed(true)
			);
		}
	}

	onMount(() => buildMap(mapDiv));

	// Filling the map with stops shape
	createEffect(() => {
		const stops = getFoundStops();
		const foundStopIds = new Set();
		for (const foundStop of stops) {
			foundStopIds.add(foundStop.id);
			foundStop.stops.forEach(s => foundStopIds.add(s.id));
		}

		for (const [stopIdStr, feature] of Object.entries(displayedFeatures)) {
			const stopId = parseInt(stopIdStr);
			if (!foundStopIds.has(stopId)) {
				console.log(`Remove feature for ${stopId}`);
				stopVectorSource.removeFeature(feature);
				delete displayedFeatures[stopId];
			}
		}

		const features = getAllMapFeatures();
		for (const [stopIdStr, feature] of Object.entries(features)) {
			const stopId = parseInt(stopIdStr);
			if (foundStopIds.has(stopId) && !(stopId in displayedFeatures)) {
				console.log(`Add feature for ${stopId}`);
				stopVectorSource.addFeature(feature);
				displayedFeatures[stopId] = feature;
			}
		}

		const extend = stopVectorSource.getExtent();
		if (map !== undefined && !isEmptyExtend(extend)) {
			map.getView().fit(extend, { duration: fitDurationMs, padding: fitPointsPadding });
		}
	});

	// Flashing effect
	createEffect(() => {
		const highlightedStopId = getHighlightedStop()?.id;
		if (highlightedStopId !== undefined) {
			const stop = getStop(highlightedStopId);
			if (stop !== undefined) {
				const stops = stop.stops ? stop.stops : [stop];
				stops.forEach((s) => {
					const feature = displayedFeatures[s.id];
					if (feature !== undefined) {
						flash(feature);
					}
				});
			}
		}
	});

	const flash = (feature: OlFeature) => {
		const start = Date.now();
		const flashGeom = feature.getGeometry()?.clone();
		const listenerKey = stopVectorLayer.on('postrender', animate);

		// Force postrender raising.
		feature.changed();

		function animate(event) {
			const frameState = event.frameState;
			const elapsed = frameState.time - start;
			const vectorContext = getVectorContext(event);

			if (elapsed >= flashDurationMs) {
				unByKey(listenerKey);
				return;
			}

			if (flashGeom !== undefined && map !== undefined) {
				const elapsedRatio = elapsed / flashDurationMs;
				// radius will be 5 at start and 30 at end.
				const radius = easeOut(elapsedRatio) * 25 + 5;
				const opacity = easeOut(1 - elapsedRatio);

				const style = new Style({
					image: new Circle({
						radius: radius,
						stroke: new Stroke({
							color: `rgba(255, 0, 0, ${opacity})`,
							width: 0.25 + opacity,
						}),
					}),
				});

				vectorContext.setStyle(style);
				vectorContext.drawGeometry(flashGeom);

				// tell OpenLayers to continue postrender animation
				map.render();
			}
		}
	}

	return <>
		<div ref={mapDiv} class="map">
			<StopPopup ref={popup} stop={selectedMapStop()} show={isPopupDisplayed()} />
		</div>
		<For each={getFoundStops()}>{(stop) => <MapStop stop={stop} selected={selectedMapStop()} />}</For>
	</>;
}
