/**
 * LeafletProvider — the default, free, no-API-key map adapter. Uses Leaflet +
 * (optionally) Leaflet.markercluster loaded globally as `window.L` (enqueued by
 * the plugin from a CDN). Infobox popups are arbitrary DOM nodes so they render
 * the chosen infobox design verbatim.
 */
import { MapProvider } from './MapProvider';
import { clusterSvg, clusterSize } from './clusterIcons';

const OSM_TILES = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
const OSM_ATTR = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';

export class LeafletProvider extends MapProvider {
	init() {
		const L = window.L;
		if ( ! L ) {
			// Leaflet not yet loaded — caller should retry once the script lands.
			return false;
		}
		this.L = L;
		const num = ( v, d ) => {
			const n = parseFloat( v );
			return Number.isFinite( n ) ? n : d;
		};
		this.map = L.map( this.el, {
			center: [ num( this.opts.centerLat, -25.2744 ), num( this.opts.centerLng, 133.7751 ) ],
			zoom: num( this.opts.zoom, 4 ),
			scrollWheelZoom: true,
		} );
		L.tileLayer( OSM_TILES, { attribution: OSM_ATTR, maxZoom: 19 } ).addTo( this.map );

		this.clustering = this.opts.clustering !== false && typeof L.markerClusterGroup === 'function';
		const clusterColor = this.opts.clusterColor;
		const clusterStyle = this.opts.clusterStyle;
		this.layer = this.clustering
			? L.markerClusterGroup( {
				// Themeable cluster bubble (style + colour from host settings).
				iconCreateFunction: ( cluster ) => {
					const count = cluster.getChildCount();
					const size = clusterSize( count );
					return L.divIcon( {
						html: clusterSvg( clusterStyle, clusterColor, count ),
						className: this.classFor( 'cluster' ),
						iconSize: [ size, size ],
					} );
				},
			} )
			: L.layerGroup();
		this.map.addLayer( this.layer );
		this.markers = new Map();
		return true;
	}

	// Build a Leaflet icon from a record's custom marker, falling back to default.
	iconFor( record ) {
		const m = this.markerOf( record );
		if ( ! m || ! m.icon_url ) return null;
		let w = m.width || 32;
		let h = m.height || 40;
		// Global marker-size setting scales every custom icon to a target height,
		// keeping the icon's aspect ratio.
		const target = Number( this.opts.markerSize );
		if ( Number.isFinite( target ) && target > 0 && h > 0 ) {
			w = Math.round( w * ( target / h ) );
			h = target;
		}
		return this.L.icon( {
			iconUrl: m.icon_url,
			iconSize: [ w, h ],
			iconAnchor: [ w / 2, h ],
			popupAnchor: [ 0, -h + 4 ],
		} );
	}

	// Build a Leaflet divIcon from a rendered HTML marker node — the design's
	// own CSS controls sizing (iconSize:null lets the injected HTML size
	// itself, since a marker design's dimensions are admin-authored and
	// variable, unlike a fixed-size icon SVG) and should anchor itself via a
	// CSS transform (e.g. translate(-50%, -100%)) rather than relying on a
	// synchronous DOM measurement here. Mirrors the proven divIcon+raw-HTML
	// pattern already used for cluster bubbles above.
	htmlIconFor( node ) {
		return this.L.divIcon( {
			html: node.outerHTML,
			className: this.classFor( 'html-marker' ),
			iconSize: null,
			iconAnchor: [ 0, 0 ],
		} );
	}

	setMarkers( records = [], { onMarkerClick, renderPopup, renderMarkerHtml } = {} ) {
		if ( ! this.map ) return;
		this.layer.clearLayers();
		this.markers.clear();
		records.forEach( ( record ) => {
			const ll = this.latLngOf( record );
			if ( ! ll ) return;
			let icon = null;
			if ( this.opts.markerType === 'html' && renderMarkerHtml ) {
				const node = renderMarkerHtml( record );
				if ( node ) icon = this.htmlIconFor( node );
			}
			if ( ! icon ) icon = this.iconFor( record );
			const marker = this.L.marker( [ ll.lat, ll.lng ], icon ? { icon } : {} );
			marker.on( 'click', () => {
				if ( onMarkerClick ) onMarkerClick( record );
				this.animateMarker( marker );
			} );
			// Bind a lazily-rendered popup so Leaflet opens it natively on click —
			// works whether or not the marker is inside a cluster.
			if ( renderPopup ) {
				marker.bindPopup( () => renderPopup( record ), {
					maxWidth: 340,
					minWidth: 240,
					closeButton: true,
					className: this.classFor( 'popup' ),
				} );
			}
			this.layer.addLayer( marker );
			this.markers.set( String( this.idOf( record ) ), marker );
		} );
		this.fitToMarkers();
	}

	// Briefly play the configured click animation on a marker's DOM icon. The host
	// supplies the keyframes for `.cliq-marker-anim--<type>` (bounce/pulse/drop); if
	// absent nothing happens, so this stays a no-op-safe enhancement.
	animateMarker( marker ) {
		const anim = this.opts.markerAnimation;
		if ( ! anim || anim === 'none' ) return;
		const el = marker && marker._icon;
		if ( ! el ) return;
		const cls = `cliq-marker-anim--${ anim }`;
		el.classList.remove( 'cliq-marker-anim', cls );
		void el.offsetWidth; // reflow → restart the animation on repeat clicks
		el.classList.add( 'cliq-marker-anim', cls );
		clearTimeout( el.__cliqAnimT );
		el.__cliqAnimT = setTimeout( () => el.classList.remove( 'cliq-marker-anim', cls ), 1000 );
	}

	// Open a specific record's popup (driven by clicking its result in the list).
	openStore( id ) {
		const marker = this.markers.get( String( id ) );
		if ( ! marker || ! this.map ) return;
		const cz = parseFloat( this.opts.clickedZoom );
		const target = Number.isFinite( cz ) ? cz : null;
		const ll = marker.getLatLng();
		if ( this.clustering && typeof this.layer.zoomToShowLayer === 'function' ) {
			// Expand the cluster to reveal the marker, then honour clicked-zoom if
			// the cluster's reveal zoom was shallower than configured.
			this.layer.zoomToShowLayer( marker, () => {
				if ( target !== null && this.map.getZoom() < target ) {
					this.map.setView( ll, target );
				}
				marker.openPopup();
			} );
		} else {
			if ( target !== null ) {
				this.map.setView( ll, target );
			}
			marker.openPopup();
		}
	}

	closePopup() {
		if ( this.map ) this.map.closePopup();
	}

	panTo( record, zoom ) {
		if ( ! this.map ) return;
		const ll = this.latLngOf( record );
		if ( ! ll ) return;
		this.map.setView( [ ll.lat, ll.lng ], zoom || Math.max( this.map.getZoom(), 12 ) );
	}

	// Centre on an explicit coordinate at an optional zoom (used to apply the
	// configured search-zoom after a location search).
	setCenter( lat, lng, zoom ) {
		if ( ! this.map ) return;
		const z = parseFloat( zoom );
		this.map.setView(
			[ parseFloat( lat ), parseFloat( lng ) ],
			Number.isFinite( z ) ? z : this.map.getZoom()
		);
	}

	fitToMarkers() {
		if ( ! this.map || ! this.markers.size ) return;
		const group = this.L.featureGroup( Array.from( this.markers.values() ) );
		try {
			this.map.fitBounds( group.getBounds().pad( 0.2 ) );
		} catch ( e ) {
			/* single/zero markers — leave the default view */
		}
	}

	drawRadius( center, meters ) {
		if ( ! this.map ) return;
		if ( this.circle ) { this.map.removeLayer( this.circle ); this.circle = null; }
		if ( ! center || ! meters ) return; // null → just clear
		this.circle = this.L.circle( [ center.lat, center.lng ], {
			radius: meters,
			color: '#3858e9',
			weight: 1,
			fillColor: '#3858e9',
			fillOpacity: 0.08,
		} ).addTo( this.map );
	}

	// Draw a route origin→dest using OSRM's free routing (no API key) as an inline
	// polyline, and render basic turn-by-turn steps into `opts.panel`. Resolves a
	// summary { distance, duration } on success, false on failure (caller may fall
	// back to the external link).
	showDirections( origin, dest, opts = {} ) {
		return import( /* webpackChunkName: "cliq-directions-leaflet" */ './LeafletDirections' )
			.then( ( module ) => module.showLeafletDirections( this, origin, dest, opts ) );
	}

	clearDirections() {
		if ( this.routeLine && this.map ) {
			this.map.removeLayer( this.routeLine );
			this.routeLine = null;
		}
	}

	destroy() {
		this.clearDirections();
		if ( this.map ) {
			this.map.remove();
			this.map = null;
		}
	}
}
