<script>
import 'maplibre-gl/dist/maplibre-gl.css';
import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl, NavigationControl } from 'maplibre-gl';
import { createApp } from 'vue';

import Lightbox from '@scripts/lightbox';

import SpotIcon from '@components/spotIcon';
import SpotIconStack from '@components/spotIconStack';
import ProjectPopup from '@components/projectPopup';
import ProjectFeed from '@components/projectFeed';
import ProjectSettings from '@components/projectSettings';

class GroupedScaleControl {
	constructor(options) {
		this.scale = new ScaleControl(options);
	}

	onAdd(map) {
		this.container = document.createElement('div');
		this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group';

		const scaleElement = this.scale.onAdd(map);
		scaleElement.classList.remove('maplibregl-ctrl');
		this.container.appendChild(scaleElement);

		return this.container;
	}

	onRemove() {
		this.scale.onRemove();
		this.container.remove();
	}
}

export default {
	components: {
		SpotIcon,
		ProjectFeed,
		ProjectSettings
	},
	data() {
		return {
			panels: {
				leftOpen: false,
				rightOpen: false
			},
			feed: null,
			settings: null,
			track: null,
			markers: [],
			markerProps: {
				project: {mainClasses: 'project', iconMain: 'marker', iconSub: 'project'},
				image: {mainClasses: 'media', iconMain: 'marker', iconSub: 'image'},
				video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
				message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
			},
			project: null,
			modeHisto: null,
			baseMaps: [],
			baseMap: null,
			map: null,
			mapInitializing: false,
			markerHeight: 32, //FIXME
			mapPadding: 16 + 32, //1rem + marker height
			maxZoom: 15,
			initialPitch: 45,
			lightbox: null,
			hikes: {
				colors: {},
				width: null,
				transitions: {
					lineWidthTransition: {duration:null}
				}
			},
			popup: {content: null, element: null},
			overview: {id: 0, codename:'overview', name: this.lang.get('project.overview')},
		};
	},
	computed: {
		projectOptions() {
			return [
				this.overview,
				...Object.values(this.projects)
			];
		}
	},
	watch: {
		baseMap(sNewBaseMap, sOldBaseMap) {
			if(this.map?.isStyleLoaded()) {
				if(sOldBaseMap && this.map.getLayer(sOldBaseMap)) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
				if(sNewBaseMap && this.map.getLayer(sNewBaseMap)) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
			}
		},
		'hash.items.0'(newProjectCodename, oldProjectCodename) { //hash.items.0 = Project Code Name
			if(newProjectCodename != oldProjectCodename) {
				this.hash.items = [newProjectCodename]; //Force removal of direct link
				this.settings.toggle(false, 0);
				this.init();
			}
		}
	},
	provide() {
		return {
			map: {
				panToBetweenPanels: this.panToBetweenPanels,
				openMarkerPopup: this.openMarkerPopup,
				closePopup: this.closePopup,
				isMarkerVisible: this.isMarkerVisible
			},
			project: this
		};
	},
	inject: ['api', 'lang', 'hash', 'projects', 'user', 'consts', 'isMobile'],
	mounted() {
		//Starts default project init() through watcher
		if(this.hash.items.length == 0) {
			this.hash.items[0] = (this.projects.getDefaultProject().mode == this.consts.modes.blog)?this.projects.getDefaultCodeName():this.overview.codename;
		}
		else this.init();
	},
	beforeUnmount() {
		this.quit();
	},
	methods: {
		async init() {
			this.initLightbox();
			this.hikes.colors = {
				'main': this.getStyleProperty('--track-main'),
				'off-track': this.getStyleProperty('--track-off-track'),
				'hitchhiking': this.getStyleProperty('--track-hitchhiking')
			};
			this.hikes.width = parseFloat(this.getStyleProperty('--track-width'));
			this.hikes.transitions.lineWidthTransition.duration = parseFloat(this.getStyleProperty('--trans-quick'));

			//Reset values
			this.track = null;
			this.project = null;
			this.removeMapContent();

			//Build Map
			this.mapInitializing = true;
			if(this.hash.items[0] && this.projects[this.hash.items[0]]) await this.initProject(this.hash.items[0]);
			else await this.initOverview();
			this.mapInitializing = false;
		},
		quit() {
			this.lightbox.end(true);
			this.lightbox = null;
			this.removeMap();
		},
		async initOverview() {
			this.modeHisto = true;
			this.hash.items = [this.overview.codename];
			this.feed.toggle(false, 0);

			await this.initOverviewMap();
		},
		async initProject(sProjectCodeName) {
			this.project = this.projects[sProjectCodeName];
			this.modeHisto = (this.project.mode == this.consts.modes.histo);

			await this.$nextTick();
			const pMapReady = this.initProjectMap();
			await this.feed.init(pMapReady);
		},
		initLightbox() {
			if(!this.lightbox) {
				this.lightbox = new Lightbox({
					alwaysShowNavOnTouchDevices: true,
					fadeDuration: parseFloat(this.getStyleProperty('--trans-quick')),
					imageFadeDuration: parseFloat(this.getStyleProperty('--trans-quick')),
					positionFromTop: 0,
					resizeDuration: parseFloat(this.getStyleProperty('--trans-slow')),
					hasVideo: true,
					onMediaChange: async (oMedia) => {
						this.hash.items = [this.project.codename, 'media', oMedia.id];
						if(oMedia.set == 'post-medias') {
							(await this.feed.goToPost('media', oMedia.id))?.panMapToMarker();
							if(!this.lightbox.hasMediaAfterCurrent()) {
								await this.feed.getNextFeed();
								await this.$nextTick();
								this.lightbox.refreshAlbum();
							}
						}
					},
					onClosing: () => {this.hash.items = [this.hash.items[0]];}
				});
			}
		},
		async initProjectMap() {
			[
				{maps: this.baseMaps, markers: this.markers},
				this.track
			] = await Promise.all([
				this.api.get('markers', {id_project: this.project.id}),
				this.api.get('geojson', {id_project: this.project.id})
			]);

			await this.initMap();
		},
		async initOverviewMap() {
			this.baseMaps = this.consts.default_maps;
			this.markers = Object.values(this.projects).map((asProject) => ({
				type: 'project',
				subtype: 'project',
				...asProject,
				opacityWhenCovered: 0.3
			}));

			await this.initMap();
		},
		async initMap() {
			//Build map
			if(!this.map) this.addMap();
			this.updateMapPadding();
						
			//Force wait for load event
			await new Promise((resolve) => {
				if(this.map.isStyleLoaded()) resolve();
				else this.map.once('load', resolve);
			});

			this.map.resize();
			this.setInitialProjectCamera();

			//Add content: Base Maps, Tracks, Markers
			this.addMapContent();

			await new Promise((resolve) => {
				if(this.map.loaded() && this.map.areTilesLoaded()) resolve();
				else this.map.once('idle', resolve);
			});
		},
		addMap() {
			this.map = new Map({
				container: 'map',
				aroundCenter: true,
				style: {
					version: 8,
					projection: {type: 'globe'},
					sky: {
						'sky-color': this.getStyleProperty('--space'),
						'horizon-color': this.getStyleProperty('--horizon'),
						'sky-horizon-blend': 0.35,
						'atmosphere-blend': 0.8
					},
					sources: {},
					layers: []
				},
				attributionControl: false
			});
			this.map.addControl(new GroupedScaleControl({unit: 'metric'}), 'bottom-right');
			this.map.addControl(new NavigationControl({showZoom: false, visualizePitch: true}), 'bottom-right');
		},
		removeMap() {
			this.removeMapContent();
			this.map?.remove();
			this.map = null;
		},
		addMapContent() {
			this.baseMaps.forEach(this.addBaseMap);
			this.addTrack();
			this.markers.forEach(this.addMarker);
		},
		removeMapContent() {
			if(!this.map) return;

			this.closePopup();
			this.removeTrack();
			this.markers.forEach(this.removeMarker);
			this.baseMaps.forEach(this.removeBaseMap);
		},
		addBaseMap(asBaseMap) {
			if(asBaseMap.default_map) this.baseMap = asBaseMap.codename;
			if(this.map.getSource(asBaseMap.codename) && this.map.getLayer(asBaseMap.codename)) return;
			this.map.addSource(asBaseMap.codename, {
				type: 'raster',
				tiles: [asBaseMap.pattern],
				tileSize: asBaseMap.tile_size
			});
			this.map.addLayer({
				id: asBaseMap.codename,
				type: 'raster',
				source: asBaseMap.codename,
				'layout': {'visibility': asBaseMap.default_map?'visible':'none'},
				minZoom: asBaseMap.min_zoom,
				maxZoom: asBaseMap.max_zoom
			});
		},
		removeBaseMap(asBaseMap) {
			if(this.map.getLayer(asBaseMap.codename)) this.map.removeLayer(asBaseMap.codename);
			if(this.map.getSource(asBaseMap.codename)) this.map.removeSource(asBaseMap.codename);
		},
		addTrack() {
			if(!this.track) return;

			this.track.features.forEach((oFeature, iFeatureId) => {
				oFeature.properties.track_id = iFeatureId;
			});
			this.map.addSource('track', {
				'type': 'geojson',
				'data': this.track
			});

			//Color mapping
			let asColorMapping = ['match', ['get', 'type']];
			for(const [sHikeType, sColor] of Object.entries(this.hikes.colors)) {
				asColorMapping.push(sHikeType);
				asColorMapping.push(sColor);
			}
			asColorMapping.push('black'); //fallback value

			//Track layer
			this.map.addLayer({
				'id': 'track',
				'type': 'line',
				'source': 'track',
				'layout': {
					'line-join': 'round',
					'line-cap': 'round'
				},
				'paint': {
					'line-color': asColorMapping,
					'line-width': this.hikes.width
				}
			});

			//Enlarged track (click hit box)
			this.map.addLayer({
				'id': 'track-hitbox',
				'type': 'line',
				'source': 'track',
				'paint': {
					'line-opacity': 0,
					'line-width': this.hikes.width + this.mapPadding
				}
			});
			this.map.on('click', 'track-hitbox', this.openTrackPopup);
			this.map.on('mouseenter', 'track-hitbox', this.onTrackHover);
			this.map.on('mouseleave', 'track-hitbox', this.onTrackHover);
		},
		removeTrack() {
			//Over clickable track
			if(this.map.getLayer('track-hitbox')) {
				this.map.off('click', 'track-hitbox', this.openTrackPopup);
				this.map.off('mouseenter', 'track-hitbox', this.onTrackHover);
				this.map.off('mouseleave', 'track-hitbox', this.onTrackHover);
				this.map.removeLayer('track-hitbox');
			}

			//Actual track
			if(this.map.getLayer('track')) this.map.removeLayer('track');

			//Track source
			if(this.map.getSource('track')) this.map.removeSource('track');
		},
		addMarker(oMarker) {
			const $Marker = document.createElement('div');
			oMarker.app = createApp(SpotIconStack, this.markerProps[oMarker.subtype]);
			oMarker.app.mount($Marker);

			oMarker.marker = new Marker({element: $Marker, anchor: 'bottom', opacityWhenCovered: oMarker.opacityWhenCovered ?? 0})
				.setLngLat([oMarker.longitude, oMarker.latitude])
				.addTo(this.map);

			const $MarkerElement = oMarker.marker.getElement();
			$MarkerElement.addEventListener('click', (oEvent) => {this.onMarkerClick(oEvent, oMarker);});
			$MarkerElement.addEventListener('mouseenter', (oEvent) => {this.onMarkerHover(oEvent, oMarker);});
			$MarkerElement.addEventListener('mouseleave', (oEvent) => {this.onMarkerHover(oEvent, oMarker);});
		},
		removeMarker(oMarker) {
			if(oMarker.app) {
				oMarker.app.unmount();
				delete oMarker.app;
			}
			if(oMarker.marker) {
				oMarker.marker.remove();
				delete oMarker.marker;
			}
		},
		onTrackHover(oEvent) {
			this.map.getCanvas().style.cursor = (oEvent.type == 'mouseenter')?'pointer':'';
		},
		onMarkerClick(oEvent, oMarker) {
			oEvent.preventDefault();
			oEvent.stopPropagation();
			switch (oMarker.type) {
				case 'project':
					this.hash.items = [oMarker.codename];
					break;
				default:
					this.openMarkerPopup(oMarker.id, oMarker.type);
			}
		},
		onMarkerHover(oEvent, oMarker) {
			switch (oMarker.type) {
				case 'project':
					if(oEvent.type == 'mouseenter') this.openProjectPopup(oMarker);
					else this.closePopup();
					break;
			}
		},
		openProjectPopup(oProject) {
			this.openPopup({
				lnglat: [oProject.longitude, oProject.latitude],
				options: oProject,
				offset: [0, -1 * this.markerHeight * this.getStyleProperty('--zoom-scale')]
			});
		},
		openMarkerPopup(iMarkerId, sMarkerType) {
			let oMarker = this.markers.find((oCandidate) => oCandidate.id == iMarkerId && oCandidate.type == sMarkerType);
			this.openPopup({
				lnglat: [oMarker.longitude, oMarker.latitude],
				options: oMarker,
				offset: [0, -1 * this.markerHeight * (this.isMobile?this.getStyleProperty('--zoom-scale'):1)]
			});
		},
		openTrackPopup(oEvent) {
			this.openPopup({
				lnglat: oEvent.lngLat,
				options: this.projects.getTrackInfo(oEvent.features[0], this.track, this.lang),
			});
		},
		openPopup({lnglat, options={}, offset=[0, 0]} = {}) {
			this.closePopup();
			const $Popup = document.createElement('div');
			this.popup.element = new Popup({
					anchor: 'bottom',
					offset: offset,
					closeButton: false
				})
				.setDOMContent($Popup)
				.setLngLat(lnglat)
				.addTo(this.map);

			this.popup.content = createApp(ProjectPopup, {
				options: options,
				project: this.project,
				hikes: this.hikes
			});
			this.popup.content
				.provide('lang', this.lang)
				.provide('consts', this.consts)
				.provide('isMobile', this.isMobile)
				.mount($Popup);
		},
		closePopup() {
			if(this.popup.content) {
				this.popup.content.unmount();
				this.popup.content = null;
			}
			if(this.popup.element) {
				this.popup.element.remove();
				this.popup.element = null;
			}
		},
		setInitialProjectCamera() {
			let oHashMarker;
			if(this.hash.items.length == 3) {
				oHashMarker = this.markers.find((oMarker) => (
					oMarker.type == this.hash.items[1] &&
					oMarker.id == this.hash.items[2] &&
					oMarker.longitude != null &&
					oMarker.latitude != null
				)) || null;
			}

			let oLastMarker = this.markers.at(-1);

			//Overview map: Center on default project
			if(!this.project) {
				//Center on default project
				const oDefaultProject = this.projects.getDefaultProject();

				//Get Map / Canvas size
				const $Canvas = this.map.getCanvas();
				const oMapBounds = this.map.getContainer().getBoundingClientRect();

				//Adapt zoom to see whole planet
				const iTargetRadius = Math.max(1, Math.min(oMapBounds.width || $Canvas.clientWidth, oMapBounds.height || $Canvas.clientHeight) / 2);
				const iWorldSize = iTargetRadius * 2 * Math.PI * Math.cos(oDefaultProject.latitude * Math.PI / 180);
				
				this.map.jumpTo({
					center: new LngLat(oDefaultProject.longitude, oDefaultProject.latitude),
					zoom: Math.log2(iWorldSize / this.map.transform.tileSize),
					pitch: 0,
					bearing: 0
				});
			}
			//Direct link to marker
			else if(oHashMarker) {
				this.map.jumpTo({
					center: new LngLat(oHashMarker.longitude, oHashMarker.latitude),
					zoom: 13,
					pitch: this.initialPitch
				});
			}
			//Blog Mode: Fit to last marker
			else if(this.project.mode == this.consts.modes.blog && oLastMarker) {
				this.map.jumpTo({
					center: new LngLat(oLastMarker.longitude, oLastMarker.latitude),
					zoom: this.maxZoom,
					pitch: this.initialPitch,
					bearing: 0
				});
			}
			//Pre Mode, Histo Mode, Blog Mode without markers or missing direct link marker: Fit to track
			else {
				let oBounds = new LngLatBounds();
				const aoTrackCoordinates = [];
				for(const iFeatureId in this.track.features) {
					oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
						(bounds, coord) => {
							aoTrackCoordinates.push(coord);
							return bounds.extend(coord);
						},
						oBounds
					);
				}

				this.map.fitBounds(oBounds, {
					padding: this.mapPadding,
					animate: false,
					maxZoom: this.maxZoom,
					pitch: this.initialPitch,
					bearing: 0
				});

				this.fixPitchedCameraCenter(aoTrackCoordinates);
			}
		},
		fixPitchedCameraCenter(aoTrackCoordinates) {
			//Project min/max coords (lat, lng) onto map rectangle corner points (x, y)
			const oScreenBounds = aoTrackCoordinates.reduce((oBounds, coord) => {
				const oPoint = this.map.project(coord);
				return {
					minX: Math.min(oBounds.minX, oPoint.x),
					minY: Math.min(oBounds.minY, oPoint.y),
					maxX: Math.max(oBounds.maxX, oPoint.x),
					maxY: Math.max(oBounds.maxY, oPoint.y)
				};
			}, {
				minX: Infinity,
				minY: Infinity,
				maxX: -Infinity,
				maxY: -Infinity
			});

			//Current Rectangle center
			const oTrackCenter = {
				x: (oScreenBounds.minX + oScreenBounds.maxX) / 2,
				y: (oScreenBounds.minY + oScreenBounds.maxY) / 2
			};

			//Convert back center point (x, y) to coords and Move map to the track center
			this.map.jumpTo({
				center: this.map.unproject([
					oTrackCenter.x,
					oTrackCenter.y
				])
			});
		},
		addNewMarkers(aoMarkers) { //FIXME Use its own marker update API
			this.markers.push(...aoMarkers);
			aoMarkers.forEach(this.addMarker);
		},
		panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) {
			return new Promise((resolve) => {
				if(!this.map) {
					resolve();
					return;
				}
				this.map.once('moveend', resolve);
				this.map.easeTo({
					center: oLngLat,
					zoom: iZoom,
					padding: this.getMapPadding(),
					duration: iAnimDuration
				});
			});
		},
		getMapPadding() {
			let bIsMobile = this.isMobile();
			return {
				top: this.mapPadding,
				bottom: this.mapPadding,
				left: this.mapPadding + ((!bIsMobile && this.panels.leftOpen && this.settings)?this.settings.getWidth():0),
				right: this.mapPadding + ((!bIsMobile && this.panels.rightOpen && this.feed)?this.feed.getWidth():0)
			};
		},
		updateMapPadding(iDuration=0) {
			const asPadding = this.getMapPadding();
			if(iDuration > 0) this.map.easeTo({padding: asPadding, duration: iDuration});
			else this.map.jumpTo({padding: asPadding});
		},
		getStyleProperty(sProperty) {
			return getComputedStyle(this.$el).getPropertyValue(sProperty).trim();
		},
		isMarkerVisible(oLngLat){
			return !!this.map && this.map.getBounds().contains(oLngLat);
		},
		onPanelToggle(sPanel, bNewValue, iAnimDuration=500) {
			const sPanelKey = sPanel + 'Open';
			let bOldValue = this.panels[sPanelKey];
			this.panels[sPanelKey] = bNewValue;

			if(bOldValue != bNewValue) {
				//Adjust map center
				if(!this.isMobile() && this.map) this.updateMapPadding(iAnimDuration);

				//Open Close panels
				this.$el.classList.toggle('with-'+sPanel+'-panel');
			}
		},
		setFeed(vPanel) {
			this.feed = vPanel;
		},
		setSettings(vPanel) {
			this.settings = vPanel;
		}
	}
}
</script>

<template>
	<div class="projects">
		<div id="space"></div>
		<div id="submap">
			<div class="loader">
				<SpotIcon :icon="'map'" :classes="'flicker'" width="fixed" />
			</div>
		</div>
		<div id="map"></div>
		<ProjectSettings
			:ref="setSettings"
			:projects="projectOptions"
			v-model:project-code-name="hash.items[0]"
			:base-maps="baseMaps"
			v-model:base-map="baseMap"
			:map-initializing="mapInitializing"
			:hikes="hikes"
			@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('left', bIsOpen, iAnimDuration)"
		/>
		<ProjectFeed
			:ref="setFeed"
			:project="project"
			:mode-histo="modeHisto"
			@request-last-update="settings?.setLastUpdate"
			@new-markers="addNewMarkers"
			@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('right', bIsOpen, iAnimDuration)"
		/>
	</div>
  </template>
