import { Button, RadioControl } from '@wordpress/components';
import { Component, createRef, Fragment } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { MapContainer, Marker, TileLayer } from 'react-leaflet';
import L from 'leaflet';
import { CheckboxControl } from '../shared/wp-form-controls';
import primaryMarkerIconUrl from '../icons/editor-marker-primary.svg';
import secondaryMarkerIconUrl from '../icons/editor-marker-secondary.svg';

import JeoGeoAutoComplete from './geo-auto-complete';
import 'leaflet/dist/leaflet.css';
import './geo-posts.css';
import { isEqual } from 'lodash-es';

function clonePoints( points = [] ) {
	return Array.isArray( points )
		? points.map( ( point ) => ( point ? { ...point } : point ) )
		: [];
}

const PRIMARY_MARKER_ICON = new L.Icon( {
	iconUrl: primaryMarkerIconUrl,
	iconSize: [ 25, 41 ],
	iconAnchor: [ 12, 41 ],
	popupAnchor: [ 0, -34 ],
} );

const SECONDARY_MARKER_ICON = new L.Icon( {
	iconUrl: secondaryMarkerIconUrl,
	iconSize: [ 25, 41 ],
	iconAnchor: [ 12, 41 ],
	popupAnchor: [ 0, -34 ],
} );

class JeoGeocodePosts extends Component {
	constructor() {
		super();
		const metadata = wp.data
			.select( 'core/editor' )
			.getEditedPostAttribute( 'meta' );
		const initialPoints = clonePoints( metadata?._related_point );

		this.state = {
			pointsCheckpoint: [],
			formMode: 'view',
			searchValue: '',
			zoom: 1,
			points: initialPoints,
			currentMarkerIndex: 0,
			loadStatus: 'pending',
			magneticMarkers: true,
		};

		this.onLocationFound = this.onLocationFound.bind( this );
		this.onMarkerDragged = this.onMarkerDragged.bind( this );
		this.getProperty = this.getProperty.bind( this );
		this.save = this.save.bind( this );
		this.clickMarkerMap = this.clickMarkerMap.bind( this );
		this.clickMarkerList = this.clickMarkerList.bind( this );
		this.mapCreated = this.mapCreated.bind( this );
		this.mapLoaded = this.mapLoaded.bind( this );
		this.relevanceClick = this.relevanceClick.bind( this );
		this.updatePoint = this.updatePoint.bind( this );
		this.updateCurrentPoint = this.updateCurrentPoint.bind( this );
		this.handleClickSave = this.handleClickSave.bind( this );
		this.onClickDelete = this.onClickDelete.bind( this );
		this.onClickEdit = this.onClickEdit.bind( this );
		this.handleSearchValue = this.handleSearchValue.bind( this );
		this.resetForm = this.resetForm.bind( this );
		this.fetchReverseGeocode = this.fetchReverseGeocode.bind( this );
		this.flyToLocation = this.flyToLocation.bind( this );
		this.renderForm = this.renderForm.bind( this );
		this.onClickNewPoint = this.onClickNewPoint.bind( this );
		this.onClickCancel = this.onClickCancel.bind( this );
		this.toggleMagnet = this.toggleMagnet.bind( this );

		this.refMap = createRef();
	}

	onClickCancel( e ) {
		e.preventDefault();
		e.stopPropagation();
		this.resetForm();
	}

	resetForm() {
		this.setState(
			( prevState ) => ( {
				formMode: 'view',
				searchValue: '',
				points: clonePoints( prevState.pointsCheckpoint ),
				pointsCheckpoint: [],
				currentMarkerIndex: 0,
			} ),
			() => {
				const { points, currentMarkerIndex } = this.state;
				if ( points.length > 0 ) {
					const activePoint = points[ currentMarkerIndex ];
					if ( activePoint ) {
						this.flyToLocation(
							activePoint._geocode_lat,
							activePoint._geocode_lon
						);
					}
				}
				this.save();
			}
		);
	}

	handleSearchValue( newValue ) {
		this.setState( {
			searchValue: newValue,
			loadStatus: 'pending',
		} );
	}

	toggleMagnet () {
		this.setState( ( prevState ) => ( {
			magneticMarkers: ! prevState.magneticMarkers,
		} ) );
	}

	/**
	 * Immutably updates info in the point object, inside the array of related points
	 *
	 * @param {number} point The Marker Index
	 * @param {Object} data the new data. Only attributes that change
	 */
	updatePoint( point, data ) {
		this.setState( ( prevState ) => ( {
			points: prevState.points.map( ( value, i ) => {
				if ( i === point ) {
					return { ...value, ...data };
				}
				return value;
			} ),
		} ) );
	}

	updateCurrentPoint( data ) {
		const { currentMarkerIndex } = this.state;
		this.updatePoint( currentMarkerIndex, data );
	}

	clickMarkerMap( e ) {
		const index = parseInt( e.target.options.id );
		this.setState(
			{
				currentMarkerIndex: index,
			},
			() => {
				const { currentMarkerIndex, points } = this.state;
				const currentPoint = points[ currentMarkerIndex ];
				this.flyToLocation(
					currentPoint._geocode_lat,
					currentPoint._geocode_lon
				);
			}
		);
	}

	clickMarkerList( e ) {
		const index = parseInt( e.target.id );
		this.setState(
			{
				currentMarkerIndex: index,
			},
			() => {
				const { currentMarkerIndex, points } = this.state;
				const currentPoint = points[ currentMarkerIndex ];
				this.flyToLocation(
					currentPoint._geocode_lat,
					currentPoint._geocode_lon
				);
			}
		);
	}

	relevanceClick( option ) {
		this.updateCurrentPoint( { relevance: option } );
	}

	fetchReverseGeocode( lat, lng ) {
		const requestUrl = new URL( jeo.ajax_url );
		requestUrl.searchParams.set( 'action', 'jeo_reverse_geocode' );
		requestUrl.searchParams.set( 'nonce', jeo.geocode_nonce );
		requestUrl.searchParams.set( 'lat', lat );
		requestUrl.searchParams.set( 'lon', lng );

		return window
			.fetch( requestUrl )
			.then( ( response ) => {
				if ( ! response.ok ) {
					throw new Error(
						__(
							'Unable to retrieve the address for the selected point.',
							'jeowp'
						)
					);
				}

				return response.json();
			} );
	}

	handleClickSave() {
		this.setState(
			{
				formMode: 'view',
				searchValue: '',
				pointsCheckpoint: [],
				loadStatus: 'pending',
			},
			() => this.save()
		);
	}

	onClickDelete( e ) {
		e.preventDefault();
		e.stopPropagation();

		const index = parseInt( e.target.attributes.marker_index.value );
		this.setState(
			( prevState ) => ( {
				points: prevState.points.filter( ( el, i ) => i !== index ),
				currentMarkerIndex: 0,
			} ),
			() => this.save()
		);
	}

	onClickEdit( e ) {
		e.preventDefault();
		e.stopPropagation();

		const { points } = this.state;
		const index = parseInt( e.target.attributes.marker_index.value );
		const point = points[ index ];
		this.setState(
			{
				pointsCheckpoint: clonePoints( points ),
				currentMarkerIndex: index,
				searchValue: point._geocode_full_address,
				formMode: 'edit',
				loadStatus: 'resolved',
			},
			() => {
				this.flyToLocation( point._geocode_lat, point._geocode_lon );
			}
		);
	}

	onClickNewPoint() {
		const { points } = this.state;

		this.setState( {
			formMode: 'new',
			pointsCheckpoint: clonePoints( points ),
			loadStatus: 'pending',
		} );
	}

	getProperty( object, property ) {
		if ( undefined !== typeof object[ property ] ) {
			return object[ property ];
		}
		return null;
	}

	onLocationFound( location ) {
		this.flyToLocation( location.lat, location.lon );
		this.fetchReverseGeocode( location.lat, location.lon ).then( ( result ) => {
			if ( result.raw.error ) {
				return this.resetForm();
			}

			const fullAddress =
				this.getProperty( result, 'full_address' ) ||
				this.getProperty( result.raw || {}, 'display_name' ) ||
				this.getProperty( location, 'full_address' ) ||
				'';

			this.setState( ( prevState ) => {
				const foundPoint = {
					_geocode_lat: prevState.magneticMarkers
						? this.getProperty( result, 'lat' )
						: String( location.lat ),
					_geocode_lon: prevState.magneticMarkers
						? this.getProperty( result, 'lon' )
						: String( location.lon ),
					_geocode_full_address: fullAddress,
					_geocode_country: this.getProperty( result, 'country' ),
					_geocode_country_code: this.getProperty( result, 'country_code' ),
					_geocode_region_level_1: this.getProperty( result, 'region_level_1' ),
					_geocode_region_level_2: this.getProperty( result, 'region_level_2' ),
					_geocode_region_level_3: this.getProperty( result, 'region_level_3' ),
					_geocode_city: this.getProperty( result, 'city' ),
					_geocode_city_level_1: this.getProperty( result, 'city_level_1' ),
				};
				const nextState = {
					searchValue: fullAddress,
					loadStatus: 'resolved',
				};

				if (
					prevState.formMode === 'new' &&
					prevState.points.length === prevState.pointsCheckpoint.length
				) {
					const existingPoint = prevState.points.filter( ( point ) =>
						isEqual( point, foundPoint )
					);

					if ( ! existingPoint.length ) {
						nextState.points = [ ...prevState.points, foundPoint ];
						nextState.currentMarkerIndex = prevState.points.length;
					}

					return nextState;
				}

				nextState.points = prevState.points.map( ( point, index ) => {
					if ( index === prevState.currentMarkerIndex ) {
						return { ...point, ...foundPoint };
					}

					return point;
				} );

				return nextState;
			} );
		} );
	}

	flyToLocation( lat, lon ) {
		this.refMap.current.flyTo( [
			parseFloat( lat ),
			parseFloat( lon ),
		] );
	}

	mapCreated( mapInstance ) {
		this.refMap.current = mapInstance;
	}

	mapLoaded( e ) {
		const { points } = this.state;

		const coords = points.map( ( point ) => {
			return [
				parseFloat( point._geocode_lat ),
				parseFloat( point._geocode_lon ),
			];
		} );

		const map = e.target;

		if ( coords.length === 1 ) {
			map.setZoom( 6 );
			map.panTo( coords[ 0 ] );
		} else if ( coords.length > 1 ) {
			map.fitBounds( coords );
		}
	}

	onMarkerDragged( e ) {
		const marker = e.target;
		const latLng = marker.getLatLng();
		this.setState( { loadStatus: 'pending' } );
		this.onLocationFound( { lat: latLng.lat, lon: latLng.lng } );
	}

	save() {
		const { points } = this.state;
		wp.data.dispatch( 'core/editor' ).editPost( {
			meta: {
				_related_point: points.filter( ( el ) => {
					return el._geocode_full_address !== null;
				} ),
			},
		} );
	}

	componentDidUpdate( prevProps, prevState ) {
		if (
			prevState.searchValue !== this.state.searchValue &&
			this.state.loadStatus !== 'resolved'
		) {
			this.setState( { loadStatus: 'pending' } );
		}
	}

	renderForm() {
		const {
			currentMarkerIndex,
			points,
			searchValue,
			formMode,
			loadStatus,
			magneticMarkers,
		} = this.state;
		const selectedPoint = points[ currentMarkerIndex ];
		const currentSearchValue = typeof searchValue === 'string'
			? searchValue
			: selectedPoint?._geocode_full_address || '';
		const isDisabled = ! (
			loadStatus === 'resolved' &&
			currentSearchValue.replace( /\s/g, '' ).length
		);
		return (
			<div>
				<div>
					<JeoGeoAutoComplete
						onSelect={ this.onLocationFound }
						value={ currentSearchValue }
						onChange={ this.handleSearchValue }
						onSearchRequest={ () => this.setState( { loadStatus: 'pending' } ) }
					/>
					<span className="jeo-geocode-search__hint">
						{ __( 'Type an address, then click Search or press Enter. You can also drag the marker across the map.', 'jeowp' ) }
					</span>
				</div>
				<div>
					{ ! isDisabled && (
						<Fragment>
							<CheckboxControl
								label={ __( 'Move marker to nearest address', 'jeowp' ) }
								checked={ magneticMarkers }
								onChange={ this.toggleMagnet }
							/>
							<RadioControl
								label={ __( 'Relevance', 'jeowp' ) }
								selected={
									points.length ? selectedPoint.relevance || 'primary' : 'primary'
								}
								options={ [
									{ label: __( 'Primary', 'jeowp' ), value: 'primary' },
									{ label: __( 'Secondary', 'jeowp' ), value: 'secondary' },
								] }
								onChange={ this.relevanceClick }
							/>
						</Fragment>
					) }
					<div className="jeo-geocode-posts__row">
						<div className="jeo-geocode-posts__buttons-list">
							<Button variant="secondary" onClick={ this.onClickCancel }>
								{ __( 'Cancel', 'jeowp' ) }
							</Button>
							<Button
								variant="primary"
								onClick={ this.handleClickSave }
								disabled={ isDisabled }
							>
								{ formMode === 'edit'
									? __( 'Update selected point', 'jeowp' )
									: __( 'Save new point', 'jeowp' ) }
							</Button>
						</div>
					</div>
				</div>
			</div>
		);
	}

	render() {
		const { currentMarkerIndex, formMode, zoom } = this.state;
		const pointsList =
			formMode !== 'new' ? this.state.points : this.state.pointsCheckpoint;
		const pointsMap = this.state.points;

		return (
			<div className="jeo-geocode-posts">
				<div className="jeo-geocode-posts__column">
					<div>
						<h2>{ __( 'Add new point', 'jeowp' ) }</h2>
						<div>
							{ formMode === 'view' ? (
								<Button variant="primary" onClick={ this.onClickNewPoint }>
									{ __( 'Add new point', 'jeowp' ) }
								</Button>
							) : formMode === 'new' ? (
								this.renderForm()
							) : null }
						</div>
					</div>
					<div>
						<h2>{ __( 'Current points', 'jeowp' ) }</h2>
						{ pointsList.length === 0 ? (
							__( 'No points', 'jeowp' )
						) : (
							<ul>
								{ pointsList.map( ( point, i ) => (
									<li
										key={ i }
										id={ i }
										onClick={
											formMode === 'view' ? this.clickMarkerList : null
										}
										className={ classNames( [
											'jeo-geocode-posts__post',
											( point && point.relevance ) || 'primary',
											currentMarkerIndex === i && 'active',
										] ) }
									>
										{ point._geocode_full_address }{ ' ' }
										{ formMode === 'view' ? (
											<Fragment>
												<Button
													variant="link"
													onClick={ this.onClickDelete }
													marker_index={ i }
												>
													{ __( 'Delete', 'jeowp' ) }
												</Button>
												<span> | </span>
												<Button
													variant="link"
													onClick={ this.onClickEdit }
													marker_index={ i }
												>
													{ __( 'Edit', 'jeowp' ) }
												</Button>
											</Fragment>
										) : (
											formMode === 'edit' &&
											currentMarkerIndex === i &&
											this.renderForm()
										) }
									</li>
								) ) }
							</ul>
						) }
					</div>
				</div>

				<div className="jeo-geocode-posts__column">
					<div id="geocode-map-container" style={ { display: 'block' } }>
						<MapContainer
							center={ [ 0, 0 ] }
							zoom={ zoom }
							whenCreated={ this.mapCreated }
							whenReady={ this.mapLoaded }
							ref={ this.refMap }
						>
							<TileLayer
								attribution='&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
								url="https://{s}.tile.osm.org/{z}/{x}/{y}.png"
							/>
							{ pointsMap.map( ( point, i ) => {
								const icon =
									! point.relevance || point.relevance === 'primary'
										? PRIMARY_MARKER_ICON
										: SECONDARY_MARKER_ICON;

								return (
									<Marker
										key={ i }
										icon={ icon }
										draggable={
											currentMarkerIndex === i && formMode !== 'view'
										}
										eventHandlers={ {
											dragend: this.onMarkerDragged,
											...( formMode === 'view'
												? { click: this.clickMarkerMap }
												: {} ),
										} }
										position={ [
											parseFloat( point._geocode_lat ),
											parseFloat( point._geocode_lon ),
										] }
										id={ i }
										opacity={ currentMarkerIndex === i ? 1 : 0.6 }
									/>
								);
							} ) }
						</MapContainer>
					</div>
				</div>
			</div>
		);
	}
}

export default JeoGeocodePosts;
