import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Checkbox, FormControlLabel, TextField } from '@mui/material';
import * as turf from '@turf/turf';
import { IconButton, Size, Type } from 'browndash-components';
import * as d3 from 'd3';
import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson';
import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl';
import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { CirclePicker, ColorResult } from 'react-color';
import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl';
import { MarkerEvent } from 'react-map-gl/dist/esm/types';
import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils';
import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
import { DocCss, Highlight } from '../../../../fields/DocSymbols';
import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { DocumentType } from '../../../documents/DocumentTypes';
import { DocUtils, Docs } from '../../../documents/Documents';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { LinkManager } from '../../../util/LinkManager';
import { SnappingManager } from '../../../util/SnappingManager';
import { UndoManager, undoable } from '../../../util/UndoManager';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
import { SidebarAnnos } from '../../SidebarAnnos';
import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
import { Colors } from '../../global/globalEnums';
import { DocumentView } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { FormattedTextBox } from '../formattedText/FormattedTextBox';
import { PinProps, PresBox } from '../trails';
import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons';
import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility';
import { MapAnchorMenu } from './MapAnchorMenu';
import './MapBox.scss';
import { MapboxApiUtility, TransportationType } from './MapboxApiUtility';
import { MarkerIcons } from './MarkerIcons';
// import { GeocoderControl } from './GeocoderControl';

// amongus
/**
 * MapBox architecture:
 * Main component: MapBox.tsx
 * Supporting Components: SidebarAnnos, CollectionStackingView
 *
 * MapBox is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content.
 * The main body of MapBox uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view.
 * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available,
 * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map).
 * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts).
 * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps
 */

const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/  and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey>
const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ';
const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';

const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';

type PopupInfo = {
    longitude: number;
    latitude: number;
    title: string;
    description: string;
};

// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & {
//     mapboxAccessToken: string;
//     marker?: Omit<MarkerProps, 'longitude' | 'latitude'>;
//     position: ControlPosition;

//     onResult: (...args: any[]) => void;
// };

type MapMarker = {
    longitude: number;
    latitude: number;
};

/**
 * Consider integrating later: allows for drawing, circling, making shapes on map
 */
// const drawingManager = new window.google.maps.drawing.DrawingManager({
//     drawingControl: true,
//     drawingControlOptions: {
//         position: google.maps.ControlPosition.TOP_RIGHT,
//         drawingModes: [
//             google.maps.drawing.OverlayType.MARKER,
//             // currently we are not supporting the following drawing mode on map, a thought for future development
//             google.maps.drawing.OverlayType.CIRCLE,
//             google.maps.drawing.OverlayType.POLYLINE,
//         ],
//     },
// });

@observer
export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
    public static LayoutString(fieldKey: string) {
        return FieldView.LayoutString(MapBox, fieldKey);
    }
    private _dragRef = React.createRef<HTMLDivElement>();
    private _sidebarRef = React.createRef<SidebarAnnos>();
    private _ref: React.RefObject<HTMLDivElement> = React.createRef();
    private _mapRef: React.RefObject<MapRef> = React.createRef();
    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void);

    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
    @computed get allSidebarDocs() {
        return DocListCast(this.dataDoc[this.SidebarKey]);
    }
    // this list contains pushpins and configs
    @computed get allAnnotations() {
        return DocListCast(this.dataDoc[this.annotationKey]);
    }
    @computed get allPushpins() {
        return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN);
    }
    @computed get allRoutes() {
        return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE);
    }
    @computed get updatedRouteCoordinates(): Feature<Geometry, GeoJsonProperties> {
        if (this.routeToAnimate?.routeCoordinates) {
            const originalCoordinates: Position[] = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates));
            // const index = Math.floor(this.animationPhase * originalCoordinates.length);
            const index = this.animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index
            console.log('Animation phase', this.animationPhase);
            const startIndex = Math.floor(index);
            const endIndex = Math.ceil(index);
            let feature: Feature<Geometry, GeoJsonProperties>;

            let geometry: LineString;
            if (startIndex === endIndex) {
                // AnimationPhase is at a whole number (no interpolation needed)
                const coordinates = [originalCoordinates[startIndex]];
                geometry = {
                    type: 'LineString',
                    coordinates,
                };
                feature = {
                    type: 'Feature',
                    properties: {
                        routeTitle: StrCast(this.routeToAnimate.title),
                    },
                    geometry: geometry,
                };
            } else {
                // Interpolate between two coordinates
                const startCoord = originalCoordinates[startIndex];
                const endCoord = originalCoordinates[endIndex];
                const fraction = index - startIndex;

                const interpolator = d3.interpolateArray(startCoord, endCoord);

                const interpolatedCoord = interpolator(fraction);

                const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]);

                geometry = {
                    type: 'LineString',
                    coordinates,
                };
                feature = {
                    type: 'Feature',
                    properties: {
                        routeTitle: StrCast(this.routeToAnimate.title),
                    },
                    geometry: geometry,
                };
            }

            autorun(() => {
                const animationUtil = this.animationUtility;
                const concattedCoordinates = geometry.coordinates.concat(originalCoordinates.slice(endIndex));
                const newFeature: Feature<LineString, turf.Properties> = {
                    type: 'Feature',
                    properties: {},
                    geometry: {
                        type: 'LineString',
                        coordinates: concattedCoordinates,
                    },
                };
                if (animationUtil) {
                    animationUtil.setPath(newFeature);
                }
            });
            return feature;
        }
        console.log('ERROR');
        return {
            type: 'Feature',
            properties: {},
            geometry: {
                type: 'LineString',
                coordinates: [],
            },
        };
    }
    @computed get selectedRouteCoordinates(): Position[] {
        let coordinates: Position[] = [];
        if (this.routeToAnimate?.routeCoordinates) {
            coordinates = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates));
        }
        return coordinates;
    }

    @computed get allRoutesGeoJson(): FeatureCollection {
        const features: Feature<Geometry, GeoJsonProperties>[] = this.allRoutes.map((routeDoc: Doc) => {
            console.log('Route coords: ', routeDoc.routeCoordinates);
            const geometry: LineString = {
                type: 'LineString',
                coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)),
            };
            return {
                type: 'Feature',
                properties: {
                    routeTitle: routeDoc.title,
                },
                geometry: geometry,
            };
        });

        return {
            type: 'FeatureCollection',
            features: features,
        };
    }

    @computed get SidebarShown() {
        return this.layoutDoc._layout_showSidebar ? true : false;
    }
    @computed get sidebarWidthPercent() {
        return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
    }
    @computed get sidebarColor() {
        return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4'));
    }
    @computed get SidebarKey() {
        return this.fieldKey + '_sidebar';
    }

    componentDidMount() {
        this._unmounting = false;
        this._props.setContentView?.(this);
    }

    _unmounting = false;
    componentWillUnmount(): void {
        this._unmounting = true;
        this.deselectPinOrRoute();
        this._rerenderTimeout && clearTimeout(this._rerenderTimeout);
        Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
    }

    /**
     * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts
     * @param doc
     * @param sidebarKey
     * @returns
     */
    sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
        if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
        const docs = doc instanceof Doc ? [doc] : doc;
        docs.forEach(doc => {
            let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPinOrRoute;
            if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) {
                existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map));
            }
            if (existingPin) {
                setTimeout(() => {
                    // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet
                    if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) {
                        const anchor = this.getAnchor(true, undefined, existingPin);
                        anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' });
                        doc.latitude = existingPin?.latitude;
                        doc.longitude = existingPin?.longitude;
                    }
                });
            }
        }); //add to annotation list

        return this.addDocument(doc, sidebarKey); // add to sidebar list
    };

    removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
        const docs = doc instanceof Doc ? [doc] : doc;
        this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined));
        return this.removeDocument(doc, annotationKey, undefined);
    };

    /**
     * Removing documents from the sidebar
     * @param doc
     * @param sidebarKey
     * @returns
     */
    sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeMapDocument(doc, sidebarKey);

    /**
     * Toggle sidebar onclick the tiny comment button on the top right corner
     * @param e
     */
    sidebarBtnDown = (e: React.PointerEvent) => {
        setupMoveUpEvents(
            this,
            e,
            (e, down, delta) =>
                runInAction(() => {
                    const localDelta = this._props
                        .ScreenToLocalTransform()
                        .scale(this._props.NativeDimScaling?.() || 1)
                        .transformDirection(delta[0], delta[1]);
                    const fullWidth = NumCast(this.layoutDoc._width);
                    const mapWidth = fullWidth - this.sidebarWidth();
                    if (this.sidebarWidth() + localDelta[0] > 0) {
                        this.layoutDoc._layout_showSidebar = true;
                        this.layoutDoc._width = fullWidth + localDelta[0];
                        this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
                    } else {
                        this.layoutDoc._layout_showSidebar = false;
                        this.layoutDoc._width = mapWidth;
                        this.layoutDoc._layout_sidebarWidthPercent = '0%';
                    }
                    return false;
                }),
            emptyFunction,
            () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map')
        );
    };
    sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth();

    /**
     * Handles toggle of sidebar on click the little comment button
     */
    @computed get sidebarHandle() {
        return (
            <div
                className="mapBox-overlayButton-sidebar"
                key="sidebar"
                title="Toggle Sidebar"
                style={{
                    display: !this._props.isContentActive() ? 'none' : undefined,
                    top: StrCast(this.Document._layout_showTitle) === 'title' ? 20 : 5,
                    backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
                }}
                onPointerDown={this.sidebarBtnDown}>
                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
            </div>
        );
    }

    // TODO: Adding highlight box layer to Maps
    @action
    toggleSidebar = () => {
        const prevWidth = this.sidebarWidth();
        this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
        this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
    };

    startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => {
        e.preventDefault();
        e.stopPropagation();

        const sourceAnchorCreator = action(() => {
            const note = this.getAnchor(true);
            if (note && this.selectedPinOrRoute) {
                note.latitude = this.selectedPinOrRoute.latitude;
                note.longitude = this.selectedPinOrRoute.longitude;
                note.map = this.selectedPinOrRoute.map;
            }
            return note as Doc;
        });

        const targetCreator = (annotationOn: Doc | undefined) => {
            const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow');
            FormattedTextBox.SetSelectOnLoad(target);
            return target;
        };
        const docView = this._props.DocumentView?.();
        docView &&
            DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                dragComplete: e => {
                    if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
                        e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this._props.Document;
                        e.annoDragData.linkSourceDoc.followLinkZoom = false;
                    }
                },
            });
    };

    createNoteAnnotation = () => {
        const createFunc = undoable(
            action(() => {
                const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]);
                if (note && this.selectedPinOrRoute) {
                    note.latitude = this.selectedPinOrRoute.latitude;
                    note.longitude = this.selectedPinOrRoute.longitude;
                    note.map = this.selectedPinOrRoute.map;
                }
            }),
            'create note annotation'
        );
        if (!this.layoutDoc.layout_showSidebar) {
            this.toggleSidebar();
            setTimeout(createFunc);
        } else createFunc();
    };
    sidebarDown = (e: React.PointerEvent) => {
        setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true);
    };
    sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
        const bounds = this._ref.current!.getBoundingClientRect();
        this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
        this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
        e.preventDefault();
        return false;
    };

    setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func);

    addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey);

    pointerEvents = () => (this._props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none');

    panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth();
    panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
    scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
    transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter];
    opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter];
    infoWidth = () => this._props.PanelWidth() / 5;
    infoHeight = () => this._props.PanelHeight() / 5;
    anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
    savedAnnotations = () => this._savedAnnotations;

    _bingSearchManager: any;
    _bingMap: any;
    get MicrosoftMaps() {
        return (window as any).Microsoft.Maps;
    }
    // uses Bing Search to retrieve lat/lng for a location.  eg.,
    //   const results = this.geocodeQuery(map.map, 'Philadelphia, PA');
    // to move the map to that location:
    //     const location = await this.geocodeQuery(this._bingMap, 'Philadelphia, PA');
    //     this._bingMap.current.setView({
    //        mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
    //        center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
    //     });
    //
    bingGeocode = (map: any, query: string) => {
        return new Promise<{ latitude: number; longitude: number }>((res, reject) => {
            //If search manager is not defined, load the search module.
            if (!this._bingSearchManager) {
                //Create an instance of the search manager and call the geocodeQuery function again.
                this.MicrosoftMaps.loadModule('Microsoft.Maps.Search', () => {
                    this._bingSearchManager = new this.MicrosoftMaps.Search.SearchManager(map.current);
                    res(this.bingGeocode(map, query));
                });
            } else {
                this._bingSearchManager.geocode({
                    where: query,
                    callback: action((r: any) => res(r.results[0].location)),
                    errorCallback: (e: any) => reject(),
                });
            }
        });
    };

    @observable
    bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string)

    geoDataRequestOptions = {
        entityType: 'PopulatedPlace',
    };

    // The pin that is selected
    @observable selectedPinOrRoute: Doc | undefined = undefined;

    @action
    deselectPinOrRoute = () => {
        if (this.selectedPinOrRoute) {
            // // Removes filter
            // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove');
            // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove');
            // Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
            // const temp = this.selectedPin;
            // if (!this._unmounting) {
            //     this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp));
            // }
            // const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude));
            // this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc));
            // if (!this._unmounting) {
            //     this._bingMap.current.entities.push(newpin);
            // }
            // this.map_docToPinMap.set(temp, newpin);
            // this.selectedPin = undefined;
            // this.bingSearchBarContents = this.Document.map;
        }
    };

    getView = async (doc: Doc) => {
        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar();
        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
    };
    /*
     * Pushpin onclick
     */
    @action
    pushpinClicked = (pinDoc: Doc) => {
        this.deselectPinOrRoute();
        this.selectedPinOrRoute = pinDoc;
        this.bingSearchBarContents = pinDoc.map;

        // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match');
        // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match');
        Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');

        this.recolorPin(this.selectedPinOrRoute, 'green');

        MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute;
        MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
        MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation;
        MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag;

        const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPinOrRoute.latitude, this.selectedPinOrRoute.longitude));
        const x = point.x + (this._props.PanelWidth() - this.sidebarWidth()) / 2;
        const y = point.y + this._props.PanelHeight() / 2 + 32;
        const cpt = this.ScreenToLocalBoxXf().inverse().transformPoint(x, y);
        MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true);

        document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
    };

    /**
     * Map OnClick
     */
    @action
    mapOnClick = (e: { location: { latitude: any; longitude: any } }) => {
        this._props.select(false);
        this.deselectPinOrRoute();
    };
    /*
     * Updates values of layout doc to match the current map
     */
    @action
    mapRecentered = () => {
        if (
            Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || //
            Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7
        ) {
            this.dataDoc.latitude = this._bingMap.current.getCenter().latitude;
            this.dataDoc.longitude = this._bingMap.current.getCenter().longitude;
            this.dataDoc.map = '';
            this.bingSearchBarContents = '';
        }
        this.dataDoc.map_zoom = this._bingMap.current.getZoom();
    };
    /*
     * Updates maptype
     */
    @action
    updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId());

    /*
     * For Bing Maps
     * Called by search button's onClick
     * Finds the geocode of the searched contents and sets location to that location
     **/
    @action
    bingSearch = () => {
        return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => {
            this.dataDoc.latitude = location.latitude;
            this.dataDoc.longitude = location.longitude;
            this.dataDoc.map_zoom = this._bingMap.current.getZoom();
            this.dataDoc.map = this.bingSearchBarContents;
        });
    };

    /*
     * Returns doc w/ relevant info
     */
    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => {
        /// this should use SELECTED pushpin for lat/long  if there is a selection, otherwise CENTER
        const anchor = Docs.Create.ConfigDocument({
            title: 'MapAnchor:' + this.Document.title,
            text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location',
            config_latitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude),
            config_longitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude),
            config_map_zoom: NumCast(this.dataDoc.map_zoom),
            // config_map_type: StrCast(this.dataDoc.map_type),
            config_map: StrCast((existingPin ?? this.selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map),
            layout_unrendered: true,
            mapPin: existingPin ?? this.selectedPinOrRoute,
            annotationOn: this.Document,
        });
        if (anchor) {
            if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
            addAsAnnotation && this.addDocument(anchor);
            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document);
            return anchor;
        }
        return this.Document;
    };

    map_docToPinMap = new Map<Doc, any>();
    map_pinHighlighted = new Map<Doc, boolean>();
    /*
     *   Input: pin doc
     *   Adds MicrosoftMaps Pushpin to the map (render)
     */
    @action
    addPushpin = (pin: Doc) => {
        const pushPin = pin.infoWindowOpen
            ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {})
            : new this.MicrosoftMaps.Pushpin(
                  new this.MicrosoftMaps.Location(pin.latitude, pin.longitude)
                  // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'}
              );

        this._bingMap.current.entities.push(pushPin);

        this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin));
        // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin));
        this.map_docToPinMap.set(pin, pushPin);
    };

    /*
     *   Input: pin doc
     *   Removes pin from annotations
     */
    @action
    removePushpinOrRoute = (pinOrRouteDoc: Doc) => this.removeMapDocument(pinOrRouteDoc, this.annotationKey);

    /*
     * Removes pushpin from map render
     */
    deletePushpin = (pinDoc: Doc) => {
        if (!this._unmounting) {
            this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc));
        }
        this.map_docToPinMap.delete(pinDoc);
        this.selectedPinOrRoute = undefined;
    };

    @action
    deleteSelectedPinOrRoute = undoable(() => {
        console.log('deleting');
        if (this.selectedPinOrRoute) {
            // Removes filter
            Doc.setDocFilter(this.Document, 'latitude', this.selectedPinOrRoute.latitude, 'remove');
            Doc.setDocFilter(this.Document, 'longitude', this.selectedPinOrRoute.longitude, 'remove');
            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPinOrRoute))}`, 'remove');

            this.removePushpinOrRoute(this.selectedPinOrRoute);
        }
        MapAnchorMenu.Instance.fadeOut(true);
        document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
    }, 'delete pin');

    tryHideMapAnchorMenu = (e: PointerEvent) => {
        let target = document.elementFromPoint(e.x, e.y);
        while (target) {
            if (target.id === 'route-destination-searcher-listbox') return;
            if (target === MapAnchorMenu.top.current) return;
            target = target.parentElement;
        }
        e.stopPropagation();
        e.preventDefault();
        MapAnchorMenu.Instance.fadeOut(true);
        runInAction(() => {
            this.temporaryRouteSource = {
                type: 'FeatureCollection',
                features: [],
            };
        });

        document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
    };

    @action
    centerOnSelectedPin = () => {
        if (this.selectedPinOrRoute) {
            this._mapRef.current?.flyTo({
                center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.latitude)],
            });
        }
        // if (this.selectedPin) {
        //     this.dataDoc.latitude = this.selectedPin.latitude;
        //     this.dataDoc.longitude = this.selectedPin.longitude;
        //     this.dataDoc.map = this.selectedPin.map ?? '';
        //     this.bingSearchBarContents = this.selectedPin.map;
        // }
        MapAnchorMenu.Instance.fadeOut(true);
        document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu);
    };

    /**
     * View options for bing maps
     */
    bingViewOptions = {
        // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng },
        zoom: this.dataDoc.latitude ?? 10,
        mapTypeId: 'grayscale',
    };

    /**
     * Map options
     */
    bingMapOptions = {
        navigationBarMode: 'square',
        backgroundColor: '#f1f3f4',
        enableInertia: true,
        supportedMapTypes: ['grayscale', 'canvasLight'],
        disableMapTypeSelectorMouseOver: true,
        // showScalebar:true
        // disableRoadView:true,
        // disableBirdseye:true
        streetsideOptions: {
            showProblemReporting: false,
            showCurrentAddress: false,
        },
    };

    recolorPin = (pin: Doc, color?: string) => {
        // this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin));
        // this.map_docToPinMap.delete(pin);
        // const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {});
        // this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin));
        // this._bingMap.current.entities.push(newpin);
        // this.map_docToPinMap.set(pin, newpin);
    };

    /*
     * Called when BingMap is first rendered
     * Initializes starting values
     */
    @observable _mapReady = false;
    @action
    bingMapReady = (map: any) => {
        this._mapReady = true;
        this._bingMap = map.map;
        if (!this._bingMap.current) {
            alert('NO Map!?');
        }
        this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick);
        this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'viewchangeend', undoable(this.mapRecentered, 'Map Layout Change'));
        this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change'));

        this._disposers.mapLocation = reaction(
            () => this.Document.map,
            mapLoc => (this.bingSearchBarContents = mapLoc),
            { fireImmediately: true }
        );
        this._disposers.highlight = reaction(
            () => this.allAnnotations.map(doc => doc[Highlight]),
            () => {
                const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin);
                allConfigPins.forEach(({ doc, pushpin }) => {
                    if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) {
                        this.recolorPin(pushpin);
                        this.map_pinHighlighted.delete(pushpin);
                    }
                });
                allConfigPins.forEach(({ doc, pushpin }) => {
                    if (doc[Highlight] && !this.map_pinHighlighted.get(pushpin)) {
                        this.recolorPin(pushpin, 'orange');
                        this.map_pinHighlighted.set(pushpin, true);
                    }
                });
            },
            { fireImmediately: true }
        );

        this._disposers.location = reaction(
            () => ({ lat: this.Document.latitude, lng: this.Document.longitude, zoom: this.Document.map_zoom, mapType: this.Document.map_type }),
            locationObject => {
                // if (this._bingMap.current)
                try {
                    locationObject?.zoom &&
                        this._bingMap.current?.setView({
                            mapTypeId: locationObject.mapType,
                            zoom: locationObject.zoom,
                            center: new this.MicrosoftMaps.Location(locationObject.lat, locationObject.lng),
                        });
                } catch (e) {
                    console.log(e);
                }
            },
            { fireImmediately: true }
        );
    };

    dragToggle = (e: React.PointerEvent) => {
        let dragClone: HTMLDivElement | undefined;

        setupMoveUpEvents(
            e,
            e,
            e => {
                // move event
                if (!dragClone) {
                    dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; // copy draggable pin
                    dragClone.style.position = 'absolute';
                    dragClone.style.zIndex = '10000';
                    DragManager.Root().appendChild(dragClone); // add clone to root
                }
                dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`;
                return false;
            },
            e => {
                // up event
                if (!dragClone) return;
                DragManager.Root().removeChild(dragClone);
                let target = document.elementFromPoint(e.x, e.y); // element for specified x and y coordinates
                while (target) {
                    if (target === this._ref.current) {
                        const cpt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY);
                        const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 2;
                        const y = cpt[1] - 20 /* height of search bar */ - this._props.PanelHeight() / 2;
                        const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y));
                        this.createPushpin(location.latitude, location.longitude);
                        break;
                    }
                    target = target.parentElement;
                }
            },
            e => {
                const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map);
                if (this.bingSearchBarContents) {
                    this.bingSearch().then(createPin);
                } else createPin();
            }
        );
    };

    // incrementer: number = 0;
    /*
     * Creates Pushpin doc and adds it to the list of annotations
     */
    @action
    createPushpin = undoable((latitude: number, longitude: number, location?: string, wikiData?: string) => {
        // Stores the pushpin as a MapMarkerDocument
        const pushpin = Docs.Create.PushpinDocument(
            NumCast(latitude),
            NumCast(longitude),
            false,
            [],
            {
                title: location ?? `lat=${NumCast(latitude)},lng=${NumCast(longitude)}`,
                map: location,
                description: '',
                wikiData: wikiData,
                markerType: 'MAP_PIN',
                markerColor: '#ff5722',
            }
            // { title: map ?? `lat=${latitude},lng=${longitude}`, map: map },
            // ,'pushpinIDamongus'+ this.incrementer++
        );
        this.addDocument(pushpin, this.annotationKey);
        console.log(pushpin);
        return pushpin;

        // mapMarker.infoWindowOpen = true;
    }, 'createpin');

    @action
    createMapRoute = undoable((coordinates: Position[], originName: string, destination: any, createPinForDestination: boolean) => {
        if (originName !== destination.place_name) {
            const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${originName} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates) });
            this.addDocument(mapRoute, this.annotationKey);
            if (createPinForDestination) {
                this.createPushpin(destination.center[1], destination.center[0], destination.place_name);
            }
            this.temporaryRouteSource = {
                type: 'FeatureCollection',
                features: [],
            };
            MapAnchorMenu.Instance.fadeOut(true);
            return mapRoute;
        }
        // TODO: Display error that can't create route to same location
    }, 'createmaproute');

    searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch();

    @observable
    featuresFromGeocodeResults: any[] = [];

    @action
    addMarkerForFeature = (feature: any) => {
        const location = feature.place_name;
        if (feature.center) {
            const longitude = feature.center[0];
            const latitude = feature.center[1];
            const wikiData = feature.properties?.wikiData;

            this.createPushpin(latitude, longitude, location, wikiData);

            if (this._mapRef.current) {
                this._mapRef.current.flyTo({
                    center: feature.center,
                });
            }
            this.featuresFromGeocodeResults = [];
        } else {
            // TODO: handle error
        }
    };

    /**
     * Makes a forward geocoding API call to Mapbox to retrieve locations based on the search input
     * @param searchText the search input (presumably a location)
     */
    handleSearchChange = async (searchText: string) => {
        const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText);
        if (features && !this.isAnimating) {
            runInAction(() => {
                this.settingsOpen = false;
                this.featuresFromGeocodeResults = features;
                this.routeToAnimate = undefined;
            });
        }
        // try {
        //     const url = MAPBOX_FORWARD_GEOCODE_BASE_URL + encodeURI(searchText) +'.json' +`?access_token=${MAPBOX_ACCESS_TOKEN}`;
        //     const response = await fetch(url);
        //     const data = await response.json();
        //     runInAction(() => {
        //         this.featuresFromGeocodeResults = data.features;
        //     })
        // } catch (error: any){
        //     // TODO: handle error in better way
        //     console.log(error);
        // }
    };
    // @action
    // debouncedCall = React.useCallback(debounce(this.debouncedOnSearchBarChange, 300), []);

    @action
    handleMapClick = (e: MapLayerMouseEvent) => {
        this.featuresFromGeocodeResults = [];
        this.settingsOpen = false;
        if (this._mapRef.current) {
            const features = this._mapRef.current.queryRenderedFeatures(e.point, {
                layers: ['map-routes-layer'],
            });

            console.error(features);
            if (features && features.length > 0 && features[0].properties && features[0].geometry) {
                const geometry = features[0].geometry as LineString;
                const routeTitle: string = features[0].properties['routeTitle'];
                const routeDoc: Doc | undefined = this.allRoutes.find(routeDoc => routeDoc.title === routeTitle);
                this.deselectPinOrRoute(); // TODO: Also deselect route if selected
                if (routeDoc) {
                    this.selectedPinOrRoute = routeDoc;
                    Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');

                    // TODO: Recolor route

                    MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute;
                    MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
                    MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation;
                    MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag;

                    MapAnchorMenu.Instance.Reset();

                    MapAnchorMenu.Instance.setRouteDoc(routeDoc);

                    // TODO: Subject to change
                    MapAnchorMenu.Instance.setAllMapboxPins(this.allAnnotations.filter(anno => !anno.layout_unrendered));

                    MapAnchorMenu.Instance.DisplayRoute = this.displayRoute;
                    MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute;
                    MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature;
                    MapAnchorMenu.Instance.OpenAnimationPanel = this.openAnimationPanel;

                    // this.selectedRouteCoordinates = geometry.coordinates;

                    MapAnchorMenu.Instance.setMenuType('route');

                    MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true);

                    document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
                }
            }
        }
    };

    /**
     * Makes a reverse geocoding API call to retrieve features corresponding to a map click (based on longitude
     * and latitude). Sets the search results accordingly.
     * @param e
     */
    handleMapDblClick = async (e: MapLayerMouseEvent) => {
        e.preventDefault();
        const lngLat: LngLat = e.lngLat;
        const longitude: number = lngLat.lng;
        const latitude: number = lngLat.lat;

        const features = await MapboxApiUtility.reverseGeocodeForFeatures(longitude, latitude);
        if (features) {
            runInAction(() => {
                this.featuresFromGeocodeResults = features;
            });
        }

        // // REVERSE GEOCODE TO GET LOCATION DETAILS
        // try {
        //     const url = MAPBOX_REVERSE_GEOCODE_BASE_URL + encodeURI(longitude.toString() + "," + latitude.toString()) + '.json' +
        //         `?access_token=${MAPBOX_ACCESS_TOKEN}`;
        //     const response = await fetch(url);
        //     const data = await response.json();
        //     console.log("REV GEOCODE DATA: ", data);
        //     runInAction(() => {
        //         this.featuresFromGeocodeResults = data.features;
        //     })
        // } catch (error: any){
        //     // TODO: handle error in better way
        //     console.log(error);
        // }
    };

    @observable
    currentPopup: PopupInfo | undefined = undefined;

    @action
    handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => {
        this.featuresFromGeocodeResults = [];
        this.deselectPinOrRoute(); // TODO: check this method
        this.selectedPinOrRoute = pinDoc;
        // this.bingSearchBarContents = pinDoc.map;

        // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match');
        // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match');
        Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');

        this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method

        MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute;
        MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
        MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation;
        MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag;

        MapAnchorMenu.Instance.Reset();

        // pass in the pinDoc
        MapAnchorMenu.Instance.setPinDoc(pinDoc);
        MapAnchorMenu.Instance.setAllMapboxPins(this.allAnnotations.filter(anno => !anno.layout_unrendered));

        MapAnchorMenu.Instance.DisplayRoute = this.displayRoute;
        MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute;
        MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature;

        MapAnchorMenu.Instance.setMenuType('standard');

        // MapAnchorMenu.Instance.jumpTo(NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3, true);

        MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true);

        document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);

        // this._mapRef.current.flyTo({
        //     center: [NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3]
        // })
    };

    @observable
    temporaryRouteSource: FeatureCollection = {
        type: 'FeatureCollection',
        features: [],
    };

    @action
    displayRoute = (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => {
        if (routeInfoMap) {
            const newTempRouteSource: FeatureCollection = {
                type: 'FeatureCollection',
                features: [
                    {
                        type: 'Feature',
                        properties: {},
                        geometry: {
                            type: 'LineString',
                            coordinates: routeInfoMap[type].coordinates,
                        },
                    },
                ],
            };
            // TODO: Create pin for destination
            // TODO: Fly to point where full route will be shown
            this.temporaryRouteSource = newTempRouteSource;
        }
    };

    @observable
    isAnimating: boolean = false;

    @observable
    routeToAnimate: Doc | undefined = undefined;

    @observable
    animationPhase: number = 0;

    @observable
    finishedFlyTo: boolean = false;

    @action
    setAnimationPhase = (newValue: number) => {
        this.animationPhase = newValue;
    };

    @observable
    frameId: number | null = null;

    @action
    setFrameId = (frameId: number) => {
        this.frameId = frameId;
    };

    @observable
    animationUtility: AnimationUtility | null = null;

    @action
    setAnimationUtility = (util: AnimationUtility) => {
        this.animationUtility = util;
    };

    @action
    openAnimationPanel = (routeDoc: Doc | undefined) => {
        if (routeDoc) {
            MapAnchorMenu.Instance.fadeOut(true);
            document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
            this.featuresFromGeocodeResults = [];
            this.routeToAnimate = routeDoc;
        }
    };

    @computed get mapboxMapViewState(): ViewState {
        return {
            zoom: NumCast(this.dataDoc.map_zoom, 8),
            longitude: NumCast(this.dataDoc.longitude, -71.4128),
            latitude: NumCast(this.dataDoc.latitude, 41.824),
            pitch: NumCast(this.dataDoc.map_pitch),
            bearing: NumCast(this.dataDoc.map_bearing),
            padding: {
                top: 0,
                bottom: 0,
                left: 0,
                right: 0,
            },
        };
    }

    @computed
    get preAnimationViewState() {
        if (!this.isAnimating) {
            return this.mapboxMapViewState;
        }
    }

    @observable
    isStreetViewAnimation: boolean = false;

    @observable
    animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM;

    @observable
    animationLineColor: string = '#ffff00';

    @action
    setAnimationLineColor = (color: ColorResult) => {
        this.animationLineColor = color.hex;
    };

    @action
    updateAnimationSpeed = () => {
        let newAnimationSpeed: AnimationSpeed;
        switch (this.animationSpeed) {
            case AnimationSpeed.SLOW:
                newAnimationSpeed = AnimationSpeed.MEDIUM;
                break;
            case AnimationSpeed.MEDIUM:
                newAnimationSpeed = AnimationSpeed.FAST;
                break;
            case AnimationSpeed.FAST:
                newAnimationSpeed = AnimationSpeed.SLOW;
                break;
            default:
                newAnimationSpeed = AnimationSpeed.MEDIUM;
                break;
        }
        this.animationSpeed = newAnimationSpeed;
        if (this.animationUtility) {
            this.animationUtility.updateAnimationSpeed(newAnimationSpeed);
        }
    };
    @computed get animationSpeedTooltipText(): string {
        switch (this.animationSpeed) {
            case AnimationSpeed.SLOW:
                return '1x speed';
            case AnimationSpeed.MEDIUM:
                return '2x speed';
            case AnimationSpeed.FAST:
                return '3x speed';
            default:
                return '2x speed';
        }
    }
    @computed get animationSpeedIcon(): JSX.Element {
        switch (this.animationSpeed) {
            case AnimationSpeed.SLOW:
                return slowSpeedIcon;
            case AnimationSpeed.MEDIUM:
                return mediumSpeedIcon;
            case AnimationSpeed.FAST:
                return fastSpeedIcon;
            default:
                return mediumSpeedIcon;
        }
    }

    @action
    toggleIsStreetViewAnimation = () => {
        const newVal = !this.isStreetViewAnimation;
        this.isStreetViewAnimation = newVal;
        if (this.animationUtility) {
            this.animationUtility.updateIsStreetViewAnimation(newVal);
        }
    };

    @observable
    dynamicRouteFeature: Feature<Geometry, GeoJsonProperties> = {
        type: 'Feature',
        properties: {},
        geometry: {
            type: 'LineString',
            coordinates: [],
        },
    };

    @observable
    path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = {
        type: 'Feature',
        geometry: {
            type: 'LineString',
            coordinates: [],
        },
        properties: {},
    };

    getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => {
        const geometry: LineString = {
            type: 'LineString',
            coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)),
        };
        return {
            type: 'Feature',
            properties: {
                routeTitle: routeDoc.title,
            },
            geometry: geometry,
        };
    };

    @action
    playAnimation = (status: AnimationStatus) => {
        if (!this._mapRef.current || !this.routeToAnimate) {
            return;
        }

        this.animationPhase = status === AnimationStatus.RESUME ? this.animationPhase : 0;
        this.frameId = AnimationStatus.RESUME ? this.frameId : null;
        this.finishedFlyTo = AnimationStatus.RESUME ? this.finishedFlyTo : false;

        const path = turf.lineString(this.selectedRouteCoordinates);

        this.settingsOpen = false;
        this.path = path;
        this.isAnimating = true;

        runInAction(() => {
            return new Promise<void>(async resolve => {
                const targetLngLat = {
                    lng: this.selectedRouteCoordinates[0][0],
                    lat: this.selectedRouteCoordinates[0][1],
                };

                const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this.isStreetViewAnimation, this.animationSpeed, this.showTerrain, this._mapRef.current);
                runInAction(() => {
                    this.setAnimationUtility(animationUtil);
                });

                const updateFrameId = (newFrameId: number) => {
                    this.setFrameId(newFrameId);
                };

                const updateAnimationPhase = (newAnimationPhase: number) => {
                    this.setAnimationPhase(newAnimationPhase);
                };

                if (status !== AnimationStatus.RESUME) {
                    const result = await animationUtil.flyInAndRotate({
                        map: this._mapRef.current!,
                        // targetLngLat,
                        // duration 3000
                        // startAltitude: 3000000,
                        // endAltitude: this.isStreetViewAnimation ? 80 : 12000,
                        // startBearing: 0,
                        // endBearing: -20,
                        // startPitch: 40,
                        // endPitch: this.isStreetViewAnimation ? 80 : 50,
                        updateFrameId,
                    });

                    console.log('Bearing: ', result.bearing);
                    console.log('Altitude: ', result.altitude);
                }

                runInAction(() => {
                    this.finishedFlyTo = true;
                });

                // follow the path while slowly rotating the camera, passing in the camera bearing and altitude from the previous animation
                await animationUtil.animatePath({
                    map: this._mapRef.current!,
                    // path: this.path,
                    // startBearing: -20,
                    // startAltitude: this.isStreetViewAnimation ? 80 : 12000,
                    // pitch: this.isStreetViewAnimation ? 80: 50,
                    currentAnimationPhase: this.animationPhase,
                    updateAnimationPhase,
                    updateFrameId,
                });

                // get the bounds of the linestring, use fitBounds() to animate to a final view
                const bbox3d = turf.bbox(this.path);

                const bbox2d: LngLatBoundsLike = [bbox3d[0], bbox3d[1], bbox3d[2], bbox3d[3]];

                this._mapRef.current!.fitBounds(bbox2d, {
                    duration: 3000,
                    pitch: 30,
                    bearing: 0,
                    padding: 120,
                });

                setTimeout(() => {
                    this.isStreetViewAnimation = false;
                    resolve();
                }, 10000);
            });
        });
    };

    @action
    pauseAnimation = () => {
        if (this.frameId && this.animationPhase > 0) {
            window.cancelAnimationFrame(this.frameId);
            this.frameId = null;
            this.isAnimating = false;
        }
    };

    @action
    stopAnimation = (close: boolean) => {
        if (this.frameId) {
            window.cancelAnimationFrame(this.frameId);
        }
        this.animationPhase = 0;
        this.frameId = null;
        this.finishedFlyTo = false;
        this.isAnimating = false;
        if (close) {
            this.animationSpeed = AnimationSpeed.MEDIUM;
            this.isStreetViewAnimation = false;
            this.routeToAnimate = undefined;
            this.animationUtility = null;
        }
    };

    getRouteAnimationOptions = (): JSX.Element => {
        return (
            <>
                <IconButton
                    tooltip={this.isAnimating && this.finishedFlyTo ? 'Pause Animation' : 'Play Animation'}
                    onPointerDown={() => {
                        if (this.isAnimating && this.finishedFlyTo) {
                            this.pauseAnimation();
                        } else if (this.animationPhase > 0) {
                            this.playAnimation(AnimationStatus.RESUME); // Resume from the current phase
                        } else {
                            this.playAnimation(AnimationStatus.START); // Play from the beginning
                        }
                    }}
                    icon={this.isAnimating && this.finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />}
                    color="black"
                    size={Size.MEDIUM}
                />
                {this.isAnimating && this.finishedFlyTo && (
                    <IconButton
                        tooltip="Restart animation"
                        onPointerDown={() => {
                            this.stopAnimation(false);
                            this.playAnimation(AnimationStatus.START);
                        }}
                        icon={<FontAwesomeIcon icon={faRotate as IconLookup} />}
                        color="black"
                        size={Size.MEDIUM}
                    />
                )}
                <IconButton style={{ marginRight: '10px' }} tooltip="Stop and close animation" onPointerDown={() => this.stopAnimation(true)} icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup} />} color="black" size={Size.MEDIUM} />
                <>
                    <div className="animation-suboptions">
                        <div>|</div>
                        <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this.isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} />
                        <div id="divider">|</div>
                        <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} />
                        <div id="divider">|</div>
                        <div style={{ display: 'flex', alignItems: 'center' }}>
                            <div>Select Line Color: </div>
                            <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={(color: any) => this.setAnimationLineColor(color)} />
                        </div>
                    </div>
                </>
            </>
        );
    };

    @action
    hideRoute = () => {
        this.temporaryRouteSource = {
            type: 'FeatureCollection',
            features: [],
        };
    };

    @observable
    settingsOpen: boolean = false;

    @observable
    mapStyle: string = 'mapbox://styles/mapbox/standard';

    @observable
    showTerrain: boolean = true;

    @action
    toggleSettings = () => {
        if (!this.isAnimating && this.animationPhase == 0) {
            this.featuresFromGeocodeResults = [];
            this.settingsOpen = !this.settingsOpen;
        }
    };

    @action
    changeMapStyle = (e: React.ChangeEvent<HTMLSelectElement>) => {
        this.dataDoc.map_style = e.target.value;
        // this.mapStyle = `mapbox://styles/mapbox/${e.target.value}`
    };

    @action
    onBearingChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const bearing = parseInt(e.target.value);
        if (!isNaN(bearing) && this._mapRef.current) {
            console.log('bearing change');
            const fixedBearing = Math.max(0, Math.min(360, bearing));
            this._mapRef.current.setBearing(fixedBearing);
            this.dataDoc.map_bearing = fixedBearing;
        }
    };

    @action
    onPitchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const pitch = parseInt(e.target.value);
        if (!isNaN(pitch) && this._mapRef.current) {
            console.log('pitch change');
            const fixedPitch = Math.max(0, Math.min(85, pitch));
            this._mapRef.current.setPitch(fixedPitch);
            this.dataDoc.map_pitch = fixedPitch;
        }
    };

    @action
    onZoomChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const zoom = parseInt(e.target.value);
        if (!isNaN(zoom) && this._mapRef.current) {
            const fixedZoom = Math.max(0, Math.min(16, zoom));
            this._mapRef.current.setZoom(fixedZoom);
            this.dataDoc.map_zoom = fixedZoom;
        }
    };

    @action
    onStepZoomChange = (increment: boolean) => {
        if (this._mapRef.current) {
            let newZoom: number;
            if (increment) {
                console.log('inc');
                newZoom = Math.min(16, this.mapboxMapViewState.zoom + 1);
            } else {
                console.log('dec');
                newZoom = Math.max(0, this.mapboxMapViewState.zoom - 1);
            }
            this._mapRef.current.setZoom(newZoom);
            this.dataDoc.map_zoom = newZoom;
        }
    };

    @action
    onMapZoom = (e: ViewStateChangeEvent) => (this.dataDoc.map_zoom = e.viewState.zoom);

    @action
    onMapMove = (e: ViewStateChangeEvent) => {
        this.dataDoc.longitude = e.viewState.longitude;
        this.dataDoc.latitude = e.viewState.latitude;
    };

    @action
    toggleShowTerrain = () => (this.showTerrain = !this.showTerrain);

    getMarkerIcon = (pinDoc: Doc): JSX.Element | null => {
        const markerType = StrCast(pinDoc.markerType);
        const markerColor = StrCast(pinDoc.markerColor);

        return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null;
    };

    static _firstRender = true;
    static _rerenderDelay = 500;
    _rerenderTimeout: any;
    render() {
        // bcz:  no idea what's going on here, but bings maps have some kind of bug
        // such that we need to delay rendering a second map on startup until the first map is rendered.
        this.Document[DocCss];
        if (MapBox._rerenderDelay) {
            // prettier-ignore
            this._rerenderTimeout = this._rerenderTimeout ??
                setTimeout(action(() => {
                    if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) {
                        MapBox._rerenderDelay = 0;
                    }
                    this._rerenderTimeout = undefined;
                    this.Document[DocCss] = this.Document[DocCss] + 1;
                }), MapBox._rerenderDelay);
            return null;
        }
        const scale = this._props.NativeDimScaling?.() || 1;
        const parscale = scale === 1 ? 1 : this.ScreenToLocalBoxXf().Scale ?? 1;

        const renderAnnotations = (childFilters?: () => string[]) => null;
        return (
            <div className="mapBox" ref={this._ref}>
                <div
                    className="mapBox-wrapper"
                    onWheel={e => e.stopPropagation()}
                    onPointerDown={async e => {
                        e.button === 0 && !e.ctrlKey && e.stopPropagation();
                    }}
                    style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
                    <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
                    {renderAnnotations(this.opaqueFilter)}
                    {SnappingManager.IsDragging ? null : renderAnnotations()}
                    {!this.routeToAnimate && (
                        <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}>
                            <TextField fullWidth placeholder="Enter a location" onChange={(e: any) => this.handleSearchChange(e.target.value)} />
                            <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} />
                        </div>
                    )}
                    {this.settingsOpen && !this.routeToAnimate && (
                        <div className="mapbox-settings-panel" style={{ right: `${0 + this.sidebarWidth()}px` }}>
                            <div className="mapbox-style-select">
                                <div>Map Style:</div>
                                <div>
                                    <select onChange={this.changeMapStyle} value={StrCast(this.dataDoc.map_style)}>
                                        <option value="mapbox://styles/mapbox/standard">Standard</option>
                                        <option value="mapbox://styles/mapbox/streets-v11">Streets</option>
                                        <option value="mapbox://styles/mapbox/outdoors-v12">Outdoors</option>
                                        <option value="mapbox://styles/mapbox/light-v11">Light</option>
                                        <option value="mapbox://styles/mapbox/dark-v11">Dark</option>
                                        <option value="mapbox://styles/mapbox/satellite-v9">Satellite</option>
                                        <option value="mapbox://styles/mapbox/satellite-streets-v12">Satellite Streets</option>
                                        <option value="mapbox://styles/mapbox/navigation-day-v1">Navigation Day</option>
                                        <option value="mapbox://styles/mapbox/navigation-night-v1">Navigation Night</option>
                                    </select>
                                </div>
                            </div>
                            <div className="mapbox-bearing-selection">
                                <div>Bearing: </div>
                                <input value={NumCast(this.mapboxMapViewState.bearing).toFixed(0)} type="number" onChange={this.onBearingChange} />
                            </div>
                            <div className="mapbox-pitch-selection">
                                <div>Pitch: </div>
                                <input value={NumCast(this.mapboxMapViewState.pitch).toFixed(0)} type="number" onChange={this.onPitchChange} />
                            </div>
                            <div className="mapbox-pitch-selection">
                                <div>Zoom: </div>
                                <input value={NumCast(this.mapboxMapViewState.zoom).toFixed(0)} type="number" onChange={this.onZoomChange} />
                            </div>
                            <div className="mapbox-terrain-selection">
                                <div>Show terrain: </div>
                                <input type="checkbox" checked={this.showTerrain} onChange={this.toggleShowTerrain} />
                            </div>
                        </div>
                    )}
                    {this.routeToAnimate && (
                        <div className="animation-panel" style={{ width: this.sidebarWidth() === 0 ? '100%' : `calc(100% - ${this.sidebarWidth()}px)` }}>
                            <div id="route-to-animate-title">{StrCast(this.routeToAnimate.title)}</div>
                            <div className="route-animation-options">{this.getRouteAnimationOptions()}</div>
                        </div>
                    )}
                    {this.featuresFromGeocodeResults.length > 0 && (
                        <div className="mapbox-geocoding-search-results">
                            <React.Fragment>
                                <h4>Choose a location for your pin: </h4>
                                {this.featuresFromGeocodeResults
                                    .filter(feature => feature.place_name)
                                    .map((feature, idx) => (
                                        <div
                                            key={idx}
                                            className="search-result-container"
                                            onClick={() => {
                                                this.handleSearchChange('');
                                                this.addMarkerForFeature(feature);
                                            }}>
                                            <div className="search-result-place-name">{feature.place_name}</div>
                                        </div>
                                    ))}
                            </React.Fragment>
                        </div>
                    )}
                    <MapProvider>
                        <MapboxMap
                            ref={this._mapRef}
                            mapboxAccessToken={MAPBOX_ACCESS_TOKEN}
                            viewState={this.isAnimating || this.routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }}
                            mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'}
                            style={{
                                position: 'absolute',
                                top: 0,
                                left: 0,
                                zIndex: '0',
                                width: NumCast(this.layoutDoc._width) * parscale,
                                height: NumCast(this.layoutDoc._height) * parscale,
                            }}
                            initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState}
                            onZoom={this.onMapZoom}
                            onMove={this.onMapMove}
                            onClick={this.handleMapClick}
                            onDblClick={this.handleMapDblClick}
                            terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}>
                            <Source id="mapbox-dem" type="raster-dem" url="mapbox://mapbox.mapbox-terrain-dem-v1" tileSize={512} maxzoom={14} />
                            <Source id="temporary-route" type="geojson" data={this.temporaryRouteSource} />
                            <Source id="map-routes" type="geojson" data={this.allRoutesGeoJson} />
                            <Layer id="temporary-route-layer" type="line" source="temporary-route" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#36454F', 'line-width': 4, 'line-dasharray': [1, 1] }} />
                            {!this.isAnimating && this.animationPhase == 0 && <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} />}
                            {this.routeToAnimate && (this.isAnimating || this.animationPhase > 0) && (
                                <>
                                    {!this.isStreetViewAnimation && (
                                        <>
                                            <Source id="animated-route" type="geojson" data={this.updatedRouteCoordinates} />
                                            <Layer
                                                id="dynamic-animation-line"
                                                type="line"
                                                source="animated-route"
                                                paint={{
                                                    'line-color': this.animationLineColor,
                                                    'line-width': 5,
                                                }}
                                            />
                                        </>
                                    )}
                                    <Source id="start-pin-base" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates[0], 0.04)} />
                                    <Source id="start-pin-top" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates[0], 0.25)} />
                                    <Source id="end-pin-base" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates.slice(-1)[0], 0.04)} />
                                    <Source id="end-pin-top" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates.slice(-1)[0], 0.25)} />
                                    <Layer
                                        id="start-fill-pin-base"
                                        type="fill-extrusion"
                                        source="start-pin-base"
                                        paint={{
                                            'fill-extrusion-color': '#0bfc03',
                                            'fill-extrusion-height': 1000,
                                        }}
                                    />
                                    <Layer
                                        id="start-fill-pin-top"
                                        type="fill-extrusion"
                                        source="start-pin-top"
                                        paint={{
                                            'fill-extrusion-color': '#0bfc03',
                                            'fill-extrusion-base': 1000,
                                            'fill-extrusion-height': 1200,
                                        }}
                                    />
                                    <Layer
                                        id="end-fill-pin-base"
                                        type="fill-extrusion"
                                        source="end-pin-base"
                                        paint={{
                                            'fill-extrusion-color': '#eb1c1c',
                                            'fill-extrusion-height': 1000,
                                        }}
                                    />
                                    <Layer
                                        id="end-fill-pin-top"
                                        type="fill-extrusion"
                                        source="end-pin-top"
                                        paint={{
                                            'fill-extrusion-color': '#eb1c1c',
                                            'fill-extrusion-base': 1000,
                                            'fill-extrusion-height': 1200,
                                        }}
                                    />
                                </>
                            )}

                            <>
                                {!this.isAnimating &&
                                    this.animationPhase == 0 &&
                                    this.allPushpins
                                        // .filter(anno => !anno.layout_unrendered)
                                        .map((pushpin, idx) => (
                                            <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}>
                                                {this.getMarkerIcon(pushpin)}
                                            </Marker>
                                        ))}
                            </>

                            {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => (
                                <Marker key={idx} longitude={marker.longitude} latitude={marker.latitude}/>
                            ))} */}
                        </MapboxMap>
                    </MapProvider>
                </div>
                <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                    <SidebarAnnos
                        ref={this._sidebarRef}
                        {...this._props}
                        fieldKey={this.fieldKey}
                        Document={this.Document}
                        layoutDoc={this.layoutDoc}
                        dataDoc={this.dataDoc}
                        usePanelWidth={true}
                        showSidebar={this.SidebarShown}
                        nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                        PanelWidth={this.sidebarWidth}
                        sidebarAddDocument={this.sidebarAddDocument}
                        moveDocument={this.moveDocument}
                        removeDocument={this.sidebarRemoveDocument}
                    />
                </div>
                {this.sidebarHandle}
            </div>
        );
    }
}
