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

import { Input, InputLeftAddon, InputGroup } from "@hope-ui/solid";
import {
	featureGroup as leafletFeatureGroup, LatLngLiteral as LeafletLatLngLiteral, Map as LeafletMap,
	Marker as LeafletMarker, tileLayer as leafletTileLayer
} from 'leaflet';

import { Stop } from './types';
import { PositionedPanel, renderLineTransportMode, renderLinePicto, TransportModeWeights } from './utils';

import { AppContextContext, AppContextStore } from "./appContext";
import { BusinessDataContext, BusinessDataStore } from "./businessData";

import "leaflet/dist/leaflet.css";
import "./stopsSearchMenu.scss";


type ByStopIdMarkers = Record<number, LeafletMarker[] | undefined>;

interface SearchStore {

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

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

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

	addMarkers: (stopId: number, markers: LeafletMarker[]) => void;

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

const SearchContext = createContext<SearchStore>();

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

	type Store = {
		searchText: string;
		searchInProgress: boolean;
		foundStops: Stop[];
		markers: ByStopIdMarkers;
		displayedPanelId: number;
		panels: PositionedPanel[];
	};

	const [store, setStore] = createStore<Store>({
		searchText: "", searchInProgress: false, foundStops: [], markers: {}, displayedPanelId: 0, panels: []
	});

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

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

		setStore('searchText', text);

		const { searchStopByName } = businessDataStore;
		console.log("store.searchText=", store.searchText);
		const stopsById = await searchStopByName(store.searchText);
		console.log("stopsById=", stopsById);
		setFoundStops(Object.values(stopsById));

		setStore('searchInProgress', false);
	}

	const isSearchInProgress = (): boolean => {
		return store.searchInProgress;
	}

	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 addMarkers = (stopId: number, markers: L.Marker[]): void => {
		setStore('markers', stopId, markers);
	}

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

	return (
		<SearchContext.Provider value={{
			getSearchText, setSearchText, isSearchInProgress,
			getFoundStops, setFoundStops,
			getDisplayedPanelId, setDisplayedPanelId,
			addMarkers,
			getPanels, setPanels
		}}>
			{props.children}
		</SearchContext.Provider>
	);
}


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 { isSearchInProgress, setSearchText } = searchStore;

	const onStopNameInput: JSX.EventHandler<HTMLInputElement, InputEvent> = async (event): Promise<void> => {
		/* TODO: Add a tempo before fetching stop for giving time to user to finish his request */
		const stopName = event.currentTarget.value;
		if (stopName.length >= props.minCharsNb) {
			console.log(`Fetching data for "${stopName}" stop name`);
			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>
			<div class="inputGroup">
				<InputGroup >
					<InputLeftAddon>🚉 🚏</InputLeftAddon>
					<Input onInput={onStopNameInput} readOnly={isSearchInProgress()} placeholder="Stop name..." />
				</InputGroup>
			</div>
		</div >
	);
};


const StopRepr: VoidComponent<{ stop: Stop }> = (props) => {

	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">
			<div class="name">{props.stop.name}</div>
			<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 businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
	const appContextStore: AppContextStore | undefined = useContext(AppContextContext);
	if (businessDataStore === undefined || appContextStore === undefined)
		return <div />;

	const { getLine } = businessDataStore;
	const { setDisplayedStops } = appContextStore;

	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])}>
			<div class="name">{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 Object.values(getPanels())) {
			if (panel.panel) {
				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 Map: VoidComponent<{}> = () => {

	const mapCenter: LeafletLatLngLiteral = { lat: 48.853, lng: 2.35 };

	const searchStore: SearchStore | undefined = useContext(SearchContext);
	if (searchStore === undefined)
		return <div />;

	const { addMarkers, getFoundStops } = searchStore;


	let mapDiv: any;
	let map: LeafletMap | undefined = undefined;
	const stopsLayerGroup = leafletFeatureGroup();

	const buildMap = (div: HTMLDivElement) => {
		map = new LeafletMap(div).setView(mapCenter, 11);
		leafletTileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
			attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
		}).addTo(map);
		stopsLayerGroup.addTo(map);
	}

	const setMarker = (stop: Stop): L.Marker[] => {
		const markers = [];
		if (stop.lat !== undefined && stop.lon !== undefined) {
			/* TODO: Add stop lines representation to popup. */
			markers.push(new LeafletMarker([stop.lat, stop.lon]).bindPopup(`${stop.name}`).openPopup());
		}
		else {
			for (const _stop of stop.stops) {
				markers.push(...setMarker(_stop));
			}
		}
		return markers;
	}

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

	createEffect(() => {
		/* TODO: Avoid to clear all layers... */
		stopsLayerGroup.clearLayers();

		for (const stop of getFoundStops()) {
			const markers = setMarker(stop);
			addMarkers(stop.id, markers);
			for (const marker of markers) {
				stopsLayerGroup.addLayer(marker);
			}
		}

		const stopsBound = stopsLayerGroup.getBounds();
		if (map !== undefined && Object.keys(stopsBound).length) {
			map.fitBounds(stopsBound);
		}
	});

	return <div ref={mapDiv} class="map" id="main-map" />;
}

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 MAX_STOPS_PER_PANEL = 5;

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