import { createContext, createEffect, createResource, createSignal, For, JSX, onMount, ParentComponent, Show, useContext, VoidComponent } from 'solid-js';
import { createStore } from "solid-js/store";
import { createScrollPosition } from "@solid-primitives/scroll";

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 OlPoint from 'ol/geom/Point';
import OlPolygon from 'ol/geom/Polygon';
import OlVectorSource from 'ol/source/Vector';
import { Tile as OlTileLayer, Vector as OlVectorLayer } from 'ol/layer';
import { Circle, Fill, Stroke, Style } from 'ol/style';
import { easeOut } from 'ol/easing';
import { getVectorContext } from 'ol/render';
import { unByKey } from 'ol/Observable';

import { Stop, StopShape } from './types';
import { PositionedPanel, renderLineTransportMode, renderLinePicto, ScrollingText, TransportModeWeights } from './utils';
import { AppContextContext, AppContextStore } from "./appContext";
import { BusinessDataContext, BusinessDataStore } from "./businessData";

import "./stopsSearchMenu.scss";


type ByStopIdMapFeatures = Record<number, OlFeature>;

interface SearchStore {

	getSearchText: () => string;
	setSearchText: (text: string, businessDataStore: BusinessDataStore) => Promise<void>;

	getFoundStops: () => Stop[];
	setFoundStops: (stops: Stop[]) => void;

	getDisplayedPanelId: () => number;
	setDisplayedPanelId: (panelId: number) => void;

	getPanels: () => PositionedPanel[];
	setPanels: (panels: PositionedPanel[]) => void;

	getHighlightedStop: () => Stop | undefined;
	setHighlightedStop: (stop: Stop) => void;
	resetHighlightedStop: () => void;

	getMapFeature: (stopId: number) => OlFeature | undefined;
	getAllMapFeatures: () => ByStopIdMapFeatures;
	setMapFeature: (stopId: number, feature: OlFeature) => void;
};

const SearchContext = createContext<SearchStore>();


function SearchProvider(props: { children: JSX.Element }) {

	const searchTextDelayMs = 1500;

	type Store = {
		searchText: string;
		searchPromise: Promise<void> | undefined;
		foundStops: Stop[];
		displayedPanelId: number;
		panels: PositionedPanel[];
		highlightedStop: Stop | undefined;
		mapFeatures: ByStopIdMapFeatures;
	};

	const [store, setStore] = createStore<Store>({
		searchText: "",
		searchPromise: undefined,
		foundStops: [],
		displayedPanelId: 0,
		panels: [],
		highlightedStop: undefined,
		mapFeatures: {},
	});

	const getSearchText = (): string => {
		return store.searchText;
	}

	const debounce = async (fn: (...args: any[]) => Promise<void>, delayMs: number) => {
		let timerId: number;
		return new Promise((...args) => {
			clearTimeout(timerId);
			timerId = setTimeout(fn, delayMs, ...args);
		});
	}

	const setSearchText = async (text: string, businessDataStore: BusinessDataStore): Promise<void> => {
		setStore('searchText', text);

		if (store.searchPromise === undefined) {
			const { searchStopByName } = businessDataStore;
			const promise: Promise<void> = debounce(async (onSuccess: () => void) => {
				console.log(`Fetching data for "${store.searchText}" stop name`);
				const stopsById = await searchStopByName(store.searchText);
				console.log("stopsById=", stopsById);
				setFoundStops(Object.values(stopsById));
				onSuccess();
			}, searchTextDelayMs).then(() => {
				setStore('searchPromise', undefined);
			});
			setStore('searchPromise', promise);
		}
	}

	const getFoundStops = (): Stop[] => {
		return store.foundStops;
	}

	const setFoundStops = (stops: Stop[]): void => {
		setStore('foundStops', stops);
	}

	const getDisplayedPanelId = (): number => {
		return store.displayedPanelId;
	}

	const setDisplayedPanelId = (panelId: number): void => {
		setStore('displayedPanelId', panelId);
	}

	const getPanels = (): PositionedPanel[] => {
		return store.panels;
	}

	const setPanels = (panels: PositionedPanel[]): void => {
		setStore('panels', panels);
	}

	const getHighlightedStop = (): Stop | undefined => {
		return store.highlightedStop;
	}

	const setHighlightedStop = (stop: Stop): void => {
		setStore('highlightedStop', stop);
	}

	const resetHighlightedStop = (): void => {
		setStore('highlightedStop', undefined);
	}


	const getAllMapFeatures = (): ByStopIdMapFeatures => {
		return store.mapFeatures;
	}

	const getMapFeature = (stopId: number): OlFeature | undefined => {
		return store.mapFeatures[stopId];
	}

	const setMapFeature = (stopId: number, feature: OlFeature): void => {
		setStore('mapFeatures', stopId, feature);
	};

	return (
		<SearchContext.Provider value={{
			getSearchText, setSearchText,
			getFoundStops, setFoundStops,
			getDisplayedPanelId, setDisplayedPanelId,
			getPanels, setPanels,
			getHighlightedStop, setHighlightedStop, resetHighlightedStop,
			getMapFeature, getAllMapFeatures, setMapFeature,
		}}>
			{props.children}
		</SearchContext.Provider>
	);
}

const StopNameInput: VoidComponent<{ onInput: JSX.EventHandler<HTMLInputElement, InputEvent>, leftAddon: string, placeholder: string }> = (props) => {
	return (
		<div class="stopNameInput">
			<div class="leftAddon">{props.leftAddon}</div>
			<input type="text" oninput={props.onInput} placeholder={props.placeholder} />
		</div>);
};

const Header: VoidComponent<{ title: string, minCharsNb: number }> = (props) => {

	const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
	const searchStore: SearchStore | undefined = useContext(SearchContext);

	if (businessDataStore === undefined || searchStore === undefined)
		return <div />;

	const { setSearchText } = searchStore;

	const onStopNameInput: JSX.EventHandler<HTMLInputElement, InputEvent> = async (event): Promise<void> => {
		const stopName = event.currentTarget.value;
		if (stopName.length >= props.minCharsNb) {
			await setSearchText(stopName, businessDataStore);
		}
	}

	return (
		<div class="header">
			<div class="title">
				<svg viewBox="0 0 1260 50">
					<text x="0" y="50%" dominant-baseline="middle" font-size="50" style="fill: #ffffff">
						{props.title}
					</text>
				</svg>
			</div>
			<StopNameInput onInput={onStopNameInput} leftAddon="🚉 🚏" placeholder="Stop name..." />
		</div >
	);
};


const StopRepr: VoidComponent<{ stop: Stop }> = (props) => {
	const fontSize: number = 40;

	const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
	if (businessDataStore === undefined)
		return <div />;

	const { getLine } = businessDataStore;

	const fetchLinesRepr = async (lineIds: string[]): Promise<JSX.Element[]> => {
		const reprs = [];
		for (const lineId of lineIds) {
			const line = await getLine(lineId);
			if (line !== undefined) {
				reprs.push(<div class="transportMode">{renderLineTransportMode(line)}</div>);
				reprs.push(renderLinePicto(line));
			}
		}
		return reprs;
	}

	const [lineReprs] = createResource<JSX.Element[], string[]>(props.stop.lines, fetchLinesRepr);

	return (
		<div class="stop">
			<svg class="name" viewBox={`0 0 215 ${fontSize}`}>
				<text
					x="100%" y="55%"
					dominant-baseline="middle" text-anchor="end"
					font-size={fontSize}>
					{props.stop.name}
				</text>
			</svg>
			<For each={lineReprs()}>{(line: JSX.Element) => line}</For>
		</div>
	);
}


type ByTransportModeReprs = {
	mode: JSX.Element | undefined;
	lines: Record<string, JSX.Element | JSX.Element[] | undefined>;
}

const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
	const fontSize: number = 10;

	const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
	const appContextStore: AppContextStore | undefined = useContext(AppContextContext);
	const searchStore: SearchStore | undefined = useContext(SearchContext);

	if (businessDataStore === undefined || appContextStore === undefined || searchStore === undefined)
		return <div />;

	const { getLine } = businessDataStore;
	const { setDisplayedStops } = appContextStore;
	const { setHighlightedStop, resetHighlightedStop } = searchStore;


	const fetchLinesRepr = async (stop: Stop): Promise<JSX.Element> => {
		const lineIds = new Set(stop.lines);
		const stops = stop.stops;
		for (const stop of stops) {
			stop.lines.forEach(lineIds.add, lineIds);
		}

		const byModeReprs: Record<string, ByTransportModeReprs> = {};
		for (const lineId of lineIds) {
			const line = await getLine(lineId);
			if (line !== undefined) {
				if (!(line.transportMode in byModeReprs)) {
					byModeReprs[line.transportMode] = {
						mode: <div class="transportMode">{renderLineTransportMode(line)}</div>,
						lines: {}
					};
				}
				byModeReprs[line.transportMode].lines[line.shortName] = renderLinePicto(line);
			}
		}

		const sortedTransportModes = Object.keys(byModeReprs).sort((x, y) => TransportModeWeights[x] <
			TransportModeWeights[y] ? 1 : -1);

		return (
			<div class="lineRepr">
				<For each={sortedTransportModes}>{(transportMode) => {
					const reprs = byModeReprs[transportMode];
					const lineNames = Object.keys(reprs.lines).sort((x, y) => x.localeCompare(y));
					return <>
						{reprs.mode}
						<div class="linesRepresentationMatrix">
							<For each={lineNames}>{(lineName) => reprs.lines[lineName]}</For>
						</div>
					</>
				}}
				</For>
			</div >
		);
	}
	const [lineReprs] = createResource(props.stop, fetchLinesRepr);

	return (
		<div
			class="stop"
			onClick={() => setDisplayedStops([props.stop])}
			onMouseEnter={() => setHighlightedStop(props.stop)}
			onMouseLeave={resetHighlightedStop}
		>
			<div class="name" >
				<ScrollingText height={fontSize} width={100} content={props.stop.name} />
			</div>
			{lineReprs()}
		</div>
	);
}

const StopsPanel: ParentComponent<{ stops: Stop[], show: boolean }> = (props) => {

	return (
		<div classList={{ "stopPanel": true, "displayed": props.show }}>
			<For each={props.stops.sort((x, y) => x.name.localeCompare(y.name))}>
				{(stop) => {
					return <Show when={stop.stops !== undefined} fallback={<StopRepr stop={stop} />}>
						<StopAreaRepr stop={stop} />
					</Show>;
				}}
			</For>
		</div>
	);
}

const StopsPanels: ParentComponent<{ maxStopsPerPanel: number }> = (props) => {
	const searchStore: SearchStore | undefined = useContext(SearchContext);

	if (searchStore === undefined) {
		return <div />;
	}

	const { getDisplayedPanelId, getFoundStops, getPanels, setDisplayedPanelId, setPanels } = searchStore;
	let stopsPanelsRef: HTMLDivElement | undefined = undefined
	const stopsPanelsScroll = createScrollPosition(() => stopsPanelsRef);
	const yStopsPanelsScroll = () => stopsPanelsScroll.y;

	createEffect(() => {
		yStopsPanelsScroll();

		for (const panel of getPanels()) {
			const panelDiv = panel.panel();
			const panelDivClientRect = panelDiv.getBoundingClientRect();
			if (panelDivClientRect.y > 0) {
				setDisplayedPanelId(panel.position);
				break;
			}
		}
	});

	return (
		<div ref={stopsPanelsRef} class="stopsPanels">
			{() => {
				setPanels([]);

				let newPanels = [];
				let positioneds: PositionedPanel[] = [];

				let stops: Stop[] = [];

				for (const stop of getFoundStops()) {
					if (stops.length < props.maxStopsPerPanel) {
						stops.push(stop);
					}
					else {
						const panelId = newPanels.length;
						const panel = <StopsPanel stops={stops} show={panelId == getDisplayedPanelId()} />;
						newPanels.push(panel);
						positioneds.push({ position: panelId, panel: panel });
						stops = [stop];
					}
				}
				if (stops.length) {
					const panelId = newPanels.length;
					const panel = <StopsPanel stops={stops} show={panelId == getDisplayedPanelId()} />;
					newPanels.push(panel);
					positioneds.push({ position: panelId, panel: panel });
				}

				setPanels(positioneds);

				return newPanels;
			}}
		</div>
	);
}


const StopPopup: ParentComponent<{ stop: Stop, show: boolean }> = (props) => {
	const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
	if (businessDataStore === undefined)
		return <div />;

	const { getLine, getStopDestinations } = businessDataStore;

	let popupDiv: HTMLDivElement | undefined = undefined;

	const getDestinations = async (stop: Stop): Promise<{ lineId: string, destinations: string[] }[]> => {
		let ret = [];

		if (stop !== undefined) {
			const result = await getStopDestinations(stop.id);
			for (const [lineId, destinations] of Object.entries(result)) {
				const line = await getLine(lineId);
				const linePicto = renderLinePicto(line);
				ret.push({ lineId: linePicto, destinations: destinations });
			}
		}

		return ret;
	}
	const [destinations] = createResource(() => props.stop, getDestinations);

	return (
		<div ref={popupDiv} classList={{ "popup": true, "displayed": props.show }}>
			<div class="header">{props.stop?.name}</div>
			<div class="body">
				<For each={destinations()}>
					{(dst) => {
						return <div class='line'>
							{dst.lineId}
							<div class="name">
								<ScrollingText height={10} width={130} content={dst.destinations.join('/')} />
							</div>
						</div>;
					}}
				</For>
			</div>
		</div >
	);
}


// TODO: Use boolean to set MapStop selected
const MapStop: VoidComponent<{ stop: Stop, selected: Stop | undefined }> = (props) => {
	const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
	const searchStore: SearchStore | undefined = useContext(SearchContext);
	if (businessDataStore === undefined || searchStore === undefined)
		return <div />;

	const { getStopShape } = businessDataStore;
	const { setMapFeature } = searchStore;

	const stopStyle = new Style({
		image: new Circle({
			fill: undefined,
			stroke: new Stroke({ color: '#3399CC', width: 1.5 }),
			radius: 10,
		}),
	});

	const selectedStopStyle = new Style({
		image: new Circle({
			fill: undefined,
			stroke: new Stroke({ color: 'purple', width: 2 }),
			radius: 10,
		}),
	});

	const stopAreaStyle = new Style({
		stroke: new Stroke({ color: 'red' }),
		fill: new Fill({ color: 'rgba(255,255,255,0.2)' }),
	});

	const getShape = async (stopId: number): Promise<StopShape | undefined> => {
		return await getStopShape(stopId);
	};
	const [shape] = createResource<StopShape | undefined, number>(props.stop.id, getShape);

	createEffect(() => {
		const shape_ = shape();

		if (shape_ === undefined) {
			return;
		}

		let feature = undefined;

		if (props.stop.epsg3857_x !== undefined && props.stop.epsg3857_y !== undefined) {
			const selectStopStyle = () => {
				return (props.selected?.id === props.stop.id ? selectedStopStyle : stopStyle);
			}
			feature = new OlFeature({
				geometry: new OlPoint([props.stop.epsg3857_x, props.stop.epsg3857_y]),
			});
			feature.setStyle(selectStopStyle);
		}

		else {
			let geometry = undefined;
			const areaShape = shape();
			if (areaShape !== undefined) {
				geometry = new OlPolygon([areaShape.epsg3857_points.slice(0, -1)]);
			}
			else {
				geometry = new OlPoint([props.stop.epsg3857_x, props.stop.epsg3857_y]);
			}
			feature = new OlFeature({ geometry: geometry });
			feature.setStyle(stopAreaStyle);
		}
		feature.setId(props.stop.id);

		setMapFeature(props.stop.id, feature);
	});

	return <For each={props.stop.stops}>{stop => <MapStop stop={stop} selected={props.selected} />}</For>;
}


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>
	</>;
}

const Footer: VoidComponent<{}> = () => {

	const searchStore: SearchStore | undefined = useContext(SearchContext);

	if (searchStore === undefined) {
		return <div />;
	}

	const { getDisplayedPanelId, getPanels } = searchStore;

	return (
		<div class="footer">
			<For each={getPanels()}>
				{(panel) => {
					const position = panel.position;
					return (
						<div>
							<svg viewBox="0 0 29 29">
								<circle cx="50%" cy="50%" r="13" stroke="#ffffff" stroke-width="3"
									style={{ fill: `var(--idfm-${position == getDisplayedPanelId() ? "white" : "black"})` }}
								/>
							</svg>
						</div>
					);
				}}
			</For>
		</div>
	);
}

export const StopsSearchMenu: VoidComponent = () => {

	const maxStopsPerPanel = 5;

	return (
		<div class="stopSearchMenu">
			<SearchProvider>
				<Header title="Recherche de l'arrêt..." minCharsNb={4} />
				<div class="body">
					<StopsPanels maxStopsPerPanel={maxStopsPerPanel} />
					<Map />
				</div>
				<Footer />
			</SearchProvider>
		</div>
	);
};
