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

import Lightbox from '@scripts/lightbox';
import { getOuterWidth } from '@scripts/common';

import SpotIcon from '@components/spotIcon';
import SpotIconStack from '@components/spotIconStack';
import ProjectPost from '@components/projectPost';
import ProjectPopup from '@components/projectPopup';
import ProjectNewsletter from '@components/projectNewsletter';

export default {
	components: {
		SpotIcon,
		ProjectPost,
		ProjectNewsletter,
		Simplebar
	},
	data() {
		return {
			feed: {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true},
			refreshRate: 60,
			lastUpdate: { unix_time: 0, relative_time: '', formatted_time: ''},
			feedPanelOpen: false,
			settingsPanelOpen: false,
			track: null,
			markers: [],
			markerSize: {width: 32, height: 32},
			markerProps: {
				image: {mainClasses: 'media', iconMain: 'marker', iconSub: 'image'},
				video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
				message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
			},
			currProject: {},
			modeHisto: false,
			posts: [],
			baseMaps: {},
			baseMap: null,
			map: null,
			lightbox: null,
			hikes: {
				colors:{'main':'#00ff78', 'off-track':'#0000ff', 'hitchhiking':'#FF7814'},
				width: 4
			},
			popup: {content: null, element: null}
		};
	},
	computed: {
		projectClasses() {
			return [
				this.feedPanelOpen?'with-feed':'',
				this.settingsPanelOpen?'with-settings':''
			].filter(n => n).join(' ');
		}
	},
	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) {
			if(newProjectCodename && newProjectCodename != oldProjectCodename) {
				this.hash.items = [newProjectCodename];
				this.toggleSettingsPanel(false, 'none');
				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'],
	beforeMount() {
		if(this.hash.items.length == 0) this.hash.items[0] = this.projects.getDefaultCodeName();
	},
	mounted() {
		this.init();
	},
	beforeUnmount() {
		this.quit();
	},
	methods: {
		async init() {
			this.initProject();
			this.initLightbox();
			
			await Promise.all([
				this.initFeed(),
				this.initMap()
			]);

			//Direct link post action
			if(this.hash.items.length == 3) await this.findPost(this.hash.items[1], this.hash.items[2]);
		},
		quit() {
			this.lightbox.end();
			this.$refs.feedSimpleBar.scrollElement.removeEventListener('scroll', this.onFeedScroll);
			this.setFeedUpdateTimer(-1);
			this.map.remove();
		},
		initProject() {
			this.currProject = this.projects[this.hash.items[0]];
			this.modeHisto = (this.currProject.mode == this.consts.modes.histo);
			this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true};
			this.posts = [];
			//this.baseMap = null;
			this.baseMaps = {};
		},
		initLightbox() {
			if(!this.lightbox) {
				this.lightbox = new Lightbox({
					alwaysShowNavOnTouchDevices: true,
					albumLabel: 'Media %1 / %2',
					fadeDuration: 300,
					imageFadeDuration: 400,
					positionFromTop: 0,
					resizeDuration: 400,
					hasVideo: true,
					onMediaChange: (oMedia) => {
						this.hash.items = [this.currProject.codename, 'media', oMedia.id];
						if(oMedia.set == 'post-medias') this.goToPost('media', oMedia.id)?.panMapToMarker();
					},
					onClosing: () => {this.hash.items = [this.hash.items[0]];}
				});
			}
		},
		async initFeed() {
			//Simplebar event
			this.$refs.feedSimpleBar?.scrollElement.addEventListener('scroll', this.onFeedScroll);

			//Mobile Touchscreen Events
			//TODO

			this.toggleFeedPanel(!this.isMobile(), 'none');

			//Add post Event handling
			//TODO
			
			//Get first posts batch
			await this.getNextFeed();
			this.$refs.feedSimpleBar.scrollElement.scrollTop = 0;

			//Start auto-update
			if(!this.modeHisto) this.setFeedUpdateTimer(this.refreshRate);
		},
		async initMap() {
			//Start async calls
			[
				{
					maps: this.baseMaps,
					markers: this.markers,
					last_update: this.lastUpdate
				},
				this.track
			] = await Promise.all([
				this.api.get('markers', {id_project: this.currProject.id}),
				this.api.get('geojson', {id_project: this.currProject.id})
			]);

			//Build Map
			if(this.map) this.map.remove();
			this.map = new Map({
				container: 'map',
				bounds: this.getInitialMapBounds(),
				fitBoundsOptions: {
					padding: {
						top: 20,
						bottom: 20,
						left: 20,
						right: 20 + (this.feedPanelOpen?(getOuterWidth(this.$refs.feed)):0)
					},
					animate: false,
					maxZoom: 15
				},
				style: {
					version: 8,
					sources: {},
					layers: []
				},
				attributionControl: false
			});

			//Get default basemap
			this.baseMap = this.baseMaps.find((asBM) => asBM.default_map)?.codename ?? null;

			//Force wait for load event
			await new Promise((resolve) => {
				if(this.map.loaded()) resolve();
				else this.map.once('load', resolve);
			});

			//Base maps (raster tiles)
			for(const asBaseMap of this.baseMaps) {
				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.codename == this.baseMap ? 'visible' : 'none'},
					minZoom: asBaseMap.min_zoom,
					maxZoom: asBaseMap.max_zoom
				});
			}
			
			//Add track
			this.addTrack(this.track);

			//Add Markers
			this.markers.forEach(oMarker => this.addMarker(oMarker));

			//Force wait for idle event
			await new Promise((resolve) => {
				if(this.map.loaded() && this.map.areTilesLoaded()) resolve();
				else this.map.once('idle', resolve);
			});
		},
		addTrack(oTrack, bCenter=false) {
			this.track = oTrack;
			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 + 20
				}
			});
			this.map.on('click', 'track-hitbox', this.openTrackPopup);
			this.map.on('mouseenter', 'track-hitbox', () => {this.map.getCanvas().style.cursor = 'pointer';});
			this.map.on('mouseleave', 'track-hitbox', () => {this.map.getCanvas().style.cursor = '';});
		},
		openTrackPopup(oEvent) {
			this.closePopup();
			this.openPopup({
				lnglat: oEvent.lngLat,
				options: this.projects.getTrackInfo(oEvent.features[0], this.track, this.lang),
			});
		},
		addMarker(oMarker) {
			const $Marker = document.createElement('div');
			createApp(SpotIconStack, this.markerProps[oMarker.subtype]).mount($Marker);

			new Marker({element: $Marker, anchor: 'bottom'})
				.setLngLat([oMarker.longitude, oMarker.latitude])
				.addTo(this.map)
				.getElement()
				.addEventListener('click', (oEvent) => {
					oEvent.preventDefault();
					oEvent.stopPropagation();
					this.openMarkerPopup(oMarker.id, oMarker.type);
				});
		},
		openMarkerPopup(iMarkerId, sMarkerType) {
			this.closePopup();
			let oMarker = this.markers.find((oCandidate) => oCandidate.id == iMarkerId && oCandidate.type == sMarkerType);
			this.openPopup({
				offset: [0, this.markerSize.height * -1], //FIXME
				lnglat: [oMarker.longitude, oMarker.latitude],
				options: oMarker
			});
		},
		openPopup({offset=[0, 0], lnglat, options={}} = {}) {
			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.currProject
			});
			this.popup.content
				.provide('lang', this.lang)
				.provide('consts', this.consts)
				.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;
			}
		},
		getInitialMapBounds() {
			let oBounds = new LngLatBounds();
			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;
			}

			if(oHashMarker) { //Direct link to marker
				oBounds.extend(new LngLat(oHashMarker.longitude, oHashMarker.latitude));
			}
			else if( //Blog Mode: Fit to last message
				this.currProject.mode == this.consts.modes.blog &&
				this.markers.length > 0
			) {
				let oLastMsg = this.markers.at(-1);
				oBounds.extend(new LngLat(oLastMsg.longitude, oLastMsg.latitude));
			}
			else { //Pre/Histo Mode: Fit to track
				for(const iFeatureId in this.track.features) {
					oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
						(bounds, coord) => {
							return bounds.extend(coord);
						},
						oBounds
					);
				}
			}

			return oBounds;
		},

		async findPost(sPostType, iPostId) {
			let oRef = this.goToPost(sPostType, iPostId);
			if(oRef) {
				await oRef.executeMainAction(0);
			}
			else if(!this.feed.outOfData) {
				await this.getNextFeed();
				await this.findPost(sPostType, iPostId);
			}
			else console.log('Missing element ID "'+iPostId+'" of type "'+sPostType+'"');
		},
		goToPost(sPostType, iPostId) {
			let bFound = false;
			let aoRefs = this.$refs.posts.filter((post) => {return post.postId == sPostType+'-'+iPostId;});
			if(aoRefs.length > 0) {
				let oRef = aoRefs[0];
				this.$refs.feedSimpleBar.scrollElement.scrollTop += Math.round(
					oRef.$el.getBoundingClientRect().top
					+ window.pageYOffset
					- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
				);
				
				return oRef;
			}
		},
		async getNextFeed() {
			if(!this.feed.outOfData && !this.feed.loading) {
				//Get next chunk
				this.feed.loading = true;
				let aoData = await this.api.get('next_feed', {id_project: this.currProject.id, id: this.feed.refIdLast});
				let iPostCount = Object.keys(aoData.feed).length;
				
				//Update pointers
				this.feed.outOfData = (iPostCount < this.consts.chunk_size);
				if(iPostCount > 0) {
					this.feed.refIdLast = aoData.ref_id_last;
					if(this.feed.firstChunk) this.feed.refIdFirst = aoData.ref_id_first;
				}
				
				//Add posts
				this.posts.push(...aoData.feed);

				this.feed.loading = false;
				this.feed.firstChunk = false;
			}

			return true;
		},
		onFeedScroll(oEvent) {
			const box = oEvent.currentTarget
			const content = box.querySelector('.simplebar-content')

			if ((box.scrollTop + box.clientHeight) / (content?.offsetHeight || 1) >= 0.8) this.getNextFeed();
		},
		setFeedUpdateTimer(iSeconds) {
			if(typeof this.feedTimer != 'undefined') clearTimeout(this.feedTimer);
			if(iSeconds >= 0) this.feedTimer = setTimeout(this.checkNewFeed, iSeconds * 1000);
		},
		async checkNewFeed() {
			let aoData = await this.api.get('new_feed', {id_project: this.currProject.id, id: this.feed.refIdFirst});
			const aoFeed = aoData.feed || [];
			const aoMarkers = aoData.markers || [];
			
			if(aoFeed.length > 0) {
				//Update pointer
				this.feed.refIdFirst = aoData.ref_id_first;
				
				//Add new posts
				this.posts.unshift(...aoFeed);
			}
					
			//Add new Markers
			if(aoMarkers.length > 0) {
				this.markers.push(...aoMarkers);
				aoMarkers.forEach(oMarker => this.addMarker(oMarker));
			}

			//Message Last Update
			this.lastUpdate = aoData.last_update;

			//Reschedule
			this.setFeedUpdateTimer(this.refreshRate);
		},
		panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) {
			const iXOffset = (this.settingsPanelOpen?getOuterWidth(this.$refs.settings):0) - (this.feedPanelOpen?getOuterWidth(this.$refs.feed):0);

			return new Promise((resolve) => {
				this.map.once('moveend', resolve);
				this.map.easeTo({
					center: oLngLat,
					zoom: iZoom,
					offset: [iXOffset / 2, 0],
					duration: iAnimDuration
				});
			});
		},
		isMarkerVisible(oLngLat){
			return this.map.getBounds().contains(oLngLat);
		},
		getGoogleMapsLink(asInfo) {
			return $('<a>', {
				href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z',
				title: this.lang.get('map.see_on_google'),
				target: '_blank',
				rel: 'noreferrer noopener'
			}).text(asInfo.lat_dms+' '+asInfo.lon_dms);
		},
		toggleFeedPanel(bShow, sMapAction) {
			let bOldValue = this.feedPanelOpen;
			this.feedPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.feedPanelOpen):bShow;

			if(bOldValue != this.feedPanelOpen && !this.isMobile()) {
				sMapAction = sMapAction || 'panTo';
				switch(sMapAction) {
					case 'none': 
						break;
					case 'panTo':
						this.map.panBy(
							[(this.feedPanelOpen?1:-1) * getOuterWidth(this.$refs.feed) / 2, 0],
							{duration: 500}
						);
						break;
					case 'panToInstant':
						this.map.panBy([(this.feedPanelOpen?1:-1) * getOuterWidth(this.$refs.feed) / 2, 0]);
						break;
				}
			}
		},
		toggleSettingsPanel(bShow, sMapAction) {
			let bOldValue = this.settingsPanelOpen;
			this.settingsPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.settingsPanelOpen):bShow;
			
			if(bOldValue != this.settingsPanelOpen && !this.isMobile()) {
				sMapAction = sMapAction || 'panTo';
				switch(sMapAction) {
					case 'none':
						break;
					case 'panTo':
						this.map.panBy(
							[(this.settingsPanelOpen?-1:1) * getOuterWidth(this.$refs.settings) / 2, 0],
							{duration: 500}
						);
						break;
					case 'panToInstant':
						this.map.panBy([(this.settingsPanelOpen?-1:1) * getOuterWidth(this.$refs.settings) /2, 0]);
						break;
				}
			}
		}
	}
}
</script>

<template>
	<div id="projects" :class="projectClasses">
		<div id="background"></div>
		<div id="submap">
			<div class="loader">
				<SpotIcon :icon="'map'" :classes="'flicker'" width="fixed" />
			</div>
		</div>
		<div id="map"></div>
		<div id="settings" class="map-container map-container-left" ref="settings">
			<div id="settings-panel" class="map-panel">
				<div class="settings-header">
					<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div>
					<div id="last_update" v-if="this.currProject.mode == this.consts.modes.blog && lastUpdate.unix_time > 0">
						<p><span><img src="images/spot-logo-only.svg" alt="" /></span><abbr :title="lastUpdate.formatted_time">{{ lang.get('feed.last_update')+' '+lastUpdate.relative_time }}</abbr></p>
					</div>
				</div>
				<div class="settings-sections">
					<Simplebar id="settings-sections-scrollbox">
						<div class="settings-section">
							<h1><SpotIcon :icon="'project'" width="fixed" :text="lang.get('project.hikes')" /></h1>
							<div class="settings-section-body">
								<div class="radio" v-for="project in projects" :key="'project-'+project.id">
									<input type="radio" :id="'project-'+project.id" :value="project.codename" v-model="$parent.hash.items[0]" />
									<label :for="'project-'+project.id">
										<span>{{ project.name }}</span>
										<a class="download" :href="project.gpxfilepath" :download="project.codename + '.gpx'" :title="lang.get('track.download')" @click.stop="()=>{}">
											<SpotIcon :icon="'download'" margin="left" />
										</a>
									</label>
								</div>
							</div>
						</div>
						<div class="settings-section">
							<h1><SpotIcon :icon="'map'" width="fixed" :text="lang.get('map.title')" /></h1>
							<div class="settings-section-body">
								<div class="radio" v-for="bm in baseMaps" :key="'map-'+bm.id_map">
									<input type="radio" :id="'map-'+bm.id_map" :value="bm.codename" v-model="baseMap" />
									<label :for="'map-'+bm.id_map">{{ lang.get('map.'+bm.codename) }}</label>
								</div>
							</div>
						</div>
						<div class="settings-section newsletter">
							<ProjectNewsletter />
						</div>
						<div class="settings-section admin" v-if="user.hasClearance(consts.clearances.admin)">
							<h1><SpotIcon :icon="'admin'" width="fixed" :text="lang.get('admin.title')" /></h1>
							<div class="admin-actions">
								<a class="button" href="#admin"><SpotIcon :icon="'config'" :text="lang.get('admin.config')" /></a>
								<a class="button" href="#upload"><SpotIcon :icon="'upload'" :text="lang.get('admin.upload')" /></a>
							</div>
						</div>
					</Simplebar>
				</div>
				<div class="settings-footer">
					<a href="https://git.lutran.fr/franzz/spot" :title="lang.get('credits.git')" target="_blank" rel="noopener">
						<SpotIcon :icon="'credits'" :text="lang.get('credits.project')" />
					</a>
					<span>&nbsp;{{ lang.get('credits.license') }}</span>
				</div>
			</div>
			<div :class="'map-control map-control-icon settings-control map-control-'+(isMobile()?'bottom':'top')" @click="toggleSettingsPanel">
				<SpotIcon :icon="settingsPanelOpen?'prev':'menu'" />
			</div>
			<div v-if="!isMobile()" id="legend" class="map-control settings-control map-control-bottom">
				<div v-for="(color, hikeType) in hikes.colors" class="track">
					<span class="line" :style="'background-color:'+color+'; height:'+hikes.width+'px;'"></span>
					<span class="desc">{{ lang.get('track.'+hikeType) }}</span>
				</div>
			</div>
			<div id="title" :class="'map-control settings-control map-control-'+(isMobile()?'bottom':'top')">
				<span>{{ currProject.name }}</span>
			</div>
		</div>
		<div id="feed" class="map-container map-container-right" ref="feed">
			<Simplebar id="feed-panel" class="map-panel" ref="feedSimpleBar">
				<div id="feed-header">
					<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
					<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
				</div>
				<div id="feed-posts">
					<ProjectPost v-for="post in posts" :options="post" ref="posts" />
				</div>
				<div id="feed-footer" v-if="feed.loading">
					<ProjectPost :options="{type: 'loading', headerless: true}" />
				</div>
			</Simplebar>
			<div :class="'map-control map-control-icon feed-control map-control-'+(isMobile()?'bottom':'top')" @click="toggleFeedPanel">
				<SpotIcon :icon="feedPanelOpen?'next':'post'" />
			</div>
		</div>
	</div>
  </template>
