import DOM from '../util/dom';
import LngLat from '../geo/lng_lat';
import Point from '@mapbox/point-geometry';
import smartWrap from '../util/smart_wrap';
import {bindAll, extend} from '../util/util';
import {anchorTranslate, applyAnchorClass} from './anchor';
import type {PositionAnchor} from './anchor';
import {Event, Evented} from '../util/evented';
import type Map from './map';
import Popup, {Offset} from './popup';
import type {LngLatLike} from '../geo/lng_lat';
import type {MapMouseEvent, MapTouchEvent} from './events';
import type {PointLike} from './camera';

type MarkerOptions = {
    element?: HTMLElement;
    offset?: PointLike;
    anchor?: PositionAnchor;
    color?: string;
    scale?: number;
    draggable?: boolean;
    clickTolerance?: number;
    rotation?: number;
    rotationAlignment?: string;
    pitchAlignment?: string;
};

/**
 * Creates a marker component
 * @param {Object} [options]
 * @param {HTMLElement} [options.element] DOM element to use as a marker. The default is a light blue, droplet-shaped SVG marker.
 * @param {string} [options.anchor='center'] A string indicating the part of the Marker that should be positioned closest to the coordinate set via {@link Marker#setLngLat}.
 *   Options are `'center'`, `'top'`, `'bottom'`, `'left'`, `'right'`, `'top-left'`, `'top-right'`, `'bottom-left'`, and `'bottom-right'`.
 * @param {PointLike} [options.offset] The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up.
 * @param {string} [options.color='#3FB1CE'] The color to use for the default marker if options.element is not provided. The default is light blue.
 * @param {number} [options.scale=1] The scale to use for the default marker if options.element is not provided. The default scale corresponds to a height of `41px` and a width of `27px`.
 * @param {boolean} [options.draggable=false] A boolean indicating whether or not a marker is able to be dragged to a new position on the map.
 * @param {number} [options.clickTolerance=0] The max number of pixels a user can shift the mouse pointer during a click on the marker for it to be considered a valid click (as opposed to a marker drag). The default is to inherit map's clickTolerance.
 * @param {number} [options.rotation=0] The rotation angle of the marker in degrees, relative to its respective `rotationAlignment` setting. A positive value will rotate the marker clockwise.
 * @param {string} [options.pitchAlignment='auto'] `map` aligns the `Marker` to the plane of the map. `viewport` aligns the `Marker` to the plane of the viewport. `auto` automatically matches the value of `rotationAlignment`.
 * @param {string} [options.rotationAlignment='auto'] `map` aligns the `Marker`'s rotation relative to the map, maintaining a bearing as the map rotates. `viewport` aligns the `Marker`'s rotation relative to the viewport, agnostic to map rotations. `auto` is equivalent to `viewport`.
 * @example
 * var marker = new maplibregl.Marker()
 *   .setLngLat([30.5, 50.5])
 *   .addTo(map);
 * @example
 * // Set options
 * var marker = new maplibregl.Marker({
 *     color: "#FFFFFF",
 *     draggable: true
 *   }).setLngLat([30.5, 50.5])
 *   .addTo(map);
 * @see [Add custom icons with Markers](https://maplibre.org/maplibre-gl-js-docs/example/custom-marker-icons/)
 * @see [Create a draggable Marker](https://maplibre.org/maplibre-gl-js-docs/example/drag-a-marker/)
 */
export default class Marker extends Evented {
    _map: Map;
    _anchor: PositionAnchor;
    _offset: Point;
    _element: HTMLElement;
    _popup: Popup;
    _lngLat: LngLat;
    _pos: Point;
    _color: string;
    _scale: number;
    _defaultMarker: boolean;
    _draggable: boolean;
    _clickTolerance: number;
    _isDragging: boolean;
    _state: 'inactive' | 'pending' | 'active'; // used for handling drag events
    _positionDelta: Point;
    _pointerdownPos: Point;
    _rotation: number;
    _pitchAlignment: string;
    _rotationAlignment: string;
    _originalTabIndex: string; // original tabindex of _element
    _opacityTimeout: ReturnType<typeof setTimeout>;

    constructor(options?: MarkerOptions, legacyOptions?: MarkerOptions) {
        super();
        // For backward compatibility -- the constructor used to accept the element as a
        // required first argument, before it was made optional.
        if (options instanceof HTMLElement || legacyOptions) {
            options = extend({element: options}, legacyOptions);
        }

        bindAll([
            '_update',
            '_onMove',
            '_onUp',
            '_addDragHandler',
            '_onMapClick',
            '_onKeyPress'
        ], this);

        this._anchor = options && options.anchor || 'center';
        this._color = options && options.color || '#3FB1CE';
        this._scale = options && options.scale || 1;
        this._draggable = options && options.draggable || false;
        this._clickTolerance = options && options.clickTolerance || 0;
        this._isDragging = false;
        this._state = 'inactive';
        this._rotation = options && options.rotation || 0;
        this._rotationAlignment = options && options.rotationAlignment || 'auto';
        this._pitchAlignment = options && options.pitchAlignment && options.pitchAlignment !== 'auto' ?  options.pitchAlignment : this._rotationAlignment;

        if (!options || !options.element) {
            this._defaultMarker = true;
            this._element = DOM.create('div');
            this._element.setAttribute('aria-label', 'Map marker');

            // create default map marker SVG
            const svg = DOM.createNS('http://www.w3.org/2000/svg', 'svg');
            const defaultHeight = 41;
            const defaultWidth = 27;
            svg.setAttributeNS(null, 'display', 'block');
            svg.setAttributeNS(null, 'height', `${defaultHeight}px`);
            svg.setAttributeNS(null, 'width', `${defaultWidth}px`);
            svg.setAttributeNS(null, 'viewBox', `0 0 ${defaultWidth} ${defaultHeight}`);

            const markerLarge = DOM.createNS('http://www.w3.org/2000/svg', 'g');
            markerLarge.setAttributeNS(null, 'stroke', 'none');
            markerLarge.setAttributeNS(null, 'stroke-width', '1');
            markerLarge.setAttributeNS(null, 'fill', 'none');
            markerLarge.setAttributeNS(null, 'fill-rule', 'evenodd');

            const page1 = DOM.createNS('http://www.w3.org/2000/svg', 'g');
            page1.setAttributeNS(null, 'fill-rule', 'nonzero');

            const shadow = DOM.createNS('http://www.w3.org/2000/svg', 'g');
            shadow.setAttributeNS(null, 'transform', 'translate(3.0, 29.0)');
            shadow.setAttributeNS(null, 'fill', '#000000');

            const ellipses = [
                {'rx': '10.5', 'ry': '5.25002273'},
                {'rx': '10.5', 'ry': '5.25002273'},
                {'rx': '9.5', 'ry': '4.77275007'},
                {'rx': '8.5', 'ry': '4.29549936'},
                {'rx': '7.5', 'ry': '3.81822308'},
                {'rx': '6.5', 'ry': '3.34094679'},
                {'rx': '5.5', 'ry': '2.86367051'},
                {'rx': '4.5', 'ry': '2.38636864'}
            ];

            for (const data of ellipses) {
                const ellipse = DOM.createNS('http://www.w3.org/2000/svg', 'ellipse');
                ellipse.setAttributeNS(null, 'opacity', '0.04');
                ellipse.setAttributeNS(null, 'cx', '10.5');
                ellipse.setAttributeNS(null, 'cy', '5.80029008');
                ellipse.setAttributeNS(null, 'rx', data['rx']);
                ellipse.setAttributeNS(null, 'ry', data['ry']);
                shadow.appendChild(ellipse);
            }

            const background = DOM.createNS('http://www.w3.org/2000/svg', 'g');
            background.setAttributeNS(null, 'fill', this._color);

            const bgPath = DOM.createNS('http://www.w3.org/2000/svg', 'path');
            bgPath.setAttributeNS(null, 'd', 'M27,13.5 C27,19.074644 20.250001,27.000002 14.75,34.500002 C14.016665,35.500004 12.983335,35.500004 12.25,34.500002 C6.7499993,27.000002 0,19.222562 0,13.5 C0,6.0441559 6.0441559,0 13.5,0 C20.955844,0 27,6.0441559 27,13.5 Z');

            background.appendChild(bgPath);

            const border = DOM.createNS('http://www.w3.org/2000/svg', 'g');
            border.setAttributeNS(null, 'opacity', '0.25');
            border.setAttributeNS(null, 'fill', '#000000');

            const borderPath = DOM.createNS('http://www.w3.org/2000/svg', 'path');
            borderPath.setAttributeNS(null, 'd', 'M13.5,0 C6.0441559,0 0,6.0441559 0,13.5 C0,19.222562 6.7499993,27 12.25,34.5 C13,35.522727 14.016664,35.500004 14.75,34.5 C20.250001,27 27,19.074644 27,13.5 C27,6.0441559 20.955844,0 13.5,0 Z M13.5,1 C20.415404,1 26,6.584596 26,13.5 C26,15.898657 24.495584,19.181431 22.220703,22.738281 C19.945823,26.295132 16.705119,30.142167 13.943359,33.908203 C13.743445,34.180814 13.612715,34.322738 13.5,34.441406 C13.387285,34.322738 13.256555,34.180814 13.056641,33.908203 C10.284481,30.127985 7.4148684,26.314159 5.015625,22.773438 C2.6163816,19.232715 1,15.953538 1,13.5 C1,6.584596 6.584596,1 13.5,1 Z');

            border.appendChild(borderPath);

            const maki = DOM.createNS('http://www.w3.org/2000/svg', 'g');
            maki.setAttributeNS(null, 'transform', 'translate(6.0, 7.0)');
            maki.setAttributeNS(null, 'fill', '#FFFFFF');

            const circleContainer = DOM.createNS('http://www.w3.org/2000/svg', 'g');
            circleContainer.setAttributeNS(null, 'transform', 'translate(8.0, 8.0)');

            const circle1 = DOM.createNS('http://www.w3.org/2000/svg', 'circle');
            circle1.setAttributeNS(null, 'fill', '#000000');
            circle1.setAttributeNS(null, 'opacity', '0.25');
            circle1.setAttributeNS(null, 'cx', '5.5');
            circle1.setAttributeNS(null, 'cy', '5.5');
            circle1.setAttributeNS(null, 'r', '5.4999962');

            const circle2 = DOM.createNS('http://www.w3.org/2000/svg', 'circle');
            circle2.setAttributeNS(null, 'fill', '#FFFFFF');
            circle2.setAttributeNS(null, 'cx', '5.5');
            circle2.setAttributeNS(null, 'cy', '5.5');
            circle2.setAttributeNS(null, 'r', '5.4999962');

            circleContainer.appendChild(circle1);
            circleContainer.appendChild(circle2);

            page1.appendChild(shadow);
            page1.appendChild(background);
            page1.appendChild(border);
            page1.appendChild(maki);
            page1.appendChild(circleContainer);

            svg.appendChild(page1);

            svg.setAttributeNS(null, 'height', `${defaultHeight * this._scale}px`);
            svg.setAttributeNS(null, 'width', `${defaultWidth * this._scale}px`);

            this._element.appendChild(svg);

            // if no element and no offset option given apply an offset for the default marker
            // the -14 as the y value of the default marker offset was determined as follows
            //
            // the marker tip is at the center of the shadow ellipse from the default svg
            // the y value of the center of the shadow ellipse relative to the svg top left is "shadow transform translate-y (29.0) + ellipse cy (5.80029008)"
            // offset to the svg center "height (41 / 2)" gives (29.0 + 5.80029008) - (41 / 2) and rounded for an integer pixel offset gives 14
            // negative is used to move the marker up from the center so the tip is at the Marker lngLat
            this._offset = Point.convert(options && options.offset || [0, -14]);
        } else {
            this._element = options.element;
            this._offset = Point.convert(options && options.offset || [0, 0]);
        }

        this._element.classList.add('maplibregl-marker', 'mapboxgl-marker');
        this._element.addEventListener('dragstart', (e: DragEvent) => {
            e.preventDefault();
        });
        this._element.addEventListener('mousedown', (e: MouseEvent) => {
            // prevent focusing on click
            e.preventDefault();
        });
        applyAnchorClass(this._element, this._anchor, 'marker');

        this._popup = null;
    }

    /**
     * Attaches the `Marker` to a `Map` object.
     * @param {Map} map The MapLibre GL JS map to add the marker to.
     * @returns {Marker} `this`
     * @example
     * var marker = new maplibregl.Marker()
     *   .setLngLat([30.5, 50.5])
     *   .addTo(map); // add the marker to the map
     */
    addTo(map: Map) {
        this.remove();
        this._map = map;
        map.getCanvasContainer().appendChild(this._element);
        map.on('move', this._update);
        map.on('moveend', this._update);
        this.setDraggable(this._draggable);
        this._update();

        // If we attached the `click` listener to the marker element, the popup
        // would close once the event propogated to `map` due to the
        // `Popup#_onClickClose` listener.
        this._map.on('click', this._onMapClick);

        return this;
    }

    /**
     * Removes the marker from a map
     * @example
     * var marker = new maplibregl.Marker().addTo(map);
     * marker.remove();
     * @returns {Marker} `this`
     */
    remove() {
        if (this._opacityTimeout) {
            clearTimeout(this._opacityTimeout);
            delete this._opacityTimeout;
        }
        if (this._map) {
            this._map.off('click', this._onMapClick);
            this._map.off('move', this._update);
            this._map.off('moveend', this._update);
            this._map.off('mousedown', this._addDragHandler);
            this._map.off('touchstart', this._addDragHandler);
            this._map.off('mouseup', this._onUp);
            this._map.off('touchend', this._onUp);
            this._map.off('mousemove', this._onMove);
            this._map.off('touchmove', this._onMove);
            delete this._map;
        }
        DOM.remove(this._element);
        if (this._popup) this._popup.remove();
        return this;
    }

    /**
     * Get the marker's geographical location.
     *
     * The longitude of the result may differ by a multiple of 360 degrees from the longitude previously
     * set by `setLngLat` because `Marker` wraps the anchor longitude across copies of the world to keep
     * the marker on screen.
     *
     * @returns {LngLat} A {@link LngLat} describing the marker's location.
     * @example
     * // Store the marker's longitude and latitude coordinates in a variable
     * var lngLat = marker.getLngLat();
     * // Print the marker's longitude and latitude values in the console
     * console.log('Longitude: ' + lngLat.lng + ', Latitude: ' + lngLat.lat )
     * @see [Create a draggable Marker](https://maplibre.org/maplibre-gl-js-docs/example/drag-a-marker/)
     */
    getLngLat() {
        return this._lngLat;
    }

    /**
     * Set the marker's geographical position and move it.
     * @param {LngLat} lnglat A {@link LngLat} describing where the marker should be located.
     * @returns {Marker} `this`
     * @example
     * // Create a new marker, set the longitude and latitude, and add it to the map
     * new maplibregl.Marker()
     *   .setLngLat([-65.017, -16.457])
     *   .addTo(map);
     * @see [Add custom icons with Markers](https://maplibre.org/maplibre-gl-js-docs/example/custom-marker-icons/)
     * @see [Create a draggable Marker](https://maplibre.org/maplibre-gl-js-docs/example/drag-a-marker/)
     */
    setLngLat(lnglat: LngLatLike) {
        this._lngLat = LngLat.convert(lnglat);
        this._pos = null;
        if (this._popup) this._popup.setLngLat(this._lngLat);
        this._update();
        return this;
    }

    /**
     * Returns the `Marker`'s HTML element.
     * @returns {HTMLElement} element
     */
    getElement() {
        return this._element;
    }

    /**
     * Binds a {@link Popup} to the {@link Marker}.
     * @param popup An instance of the {@link Popup} class. If undefined or null, any popup
     * set on this {@link Marker} instance is unset.
     * @returns {Marker} `this`
     * @example
     * var marker = new maplibregl.Marker()
     *  .setLngLat([0, 0])
     *  .setPopup(new maplibregl.Popup().setHTML("<h1>Hello World!</h1>")) // add popup
     *  .addTo(map);
     * @see [Attach a popup to a marker instance](https://maplibre.org/maplibre-gl-js-docs/example/set-popup/)
     */
    setPopup(popup?: Popup | null) {
        if (this._popup) {
            this._popup.remove();
            this._popup = null;
            this._element.removeEventListener('keypress', this._onKeyPress);

            if (!this._originalTabIndex) {
                this._element.removeAttribute('tabindex');
            }
        }

        if (popup) {
            if (!('offset' in popup.options)) {
                const markerHeight = 41 - (5.8 / 2);
                const markerRadius = 13.5;
                const linearOffset = Math.sqrt(Math.pow(markerRadius, 2) / 2);
                popup.options.offset = this._defaultMarker ? {
                    'top': [0, 0],
                    'top-left': [0, 0],
                    'top-right': [0, 0],
                    'bottom': [0, -markerHeight],
                    'bottom-left': [linearOffset, (markerHeight - markerRadius + linearOffset) * -1],
                    'bottom-right': [-linearOffset, (markerHeight - markerRadius + linearOffset) * -1],
                    'left': [markerRadius, (markerHeight - markerRadius) * -1],
                    'right': [-markerRadius, (markerHeight - markerRadius) * -1]
                } as Offset : this._offset;
            }
            this._popup = popup;
            if (this._lngLat) this._popup.setLngLat(this._lngLat);

            this._originalTabIndex = this._element.getAttribute('tabindex');
            if (!this._originalTabIndex) {
                this._element.setAttribute('tabindex', '0');
            }
            this._element.addEventListener('keypress', this._onKeyPress);
        }

        return this;
    }

    _onKeyPress(e: KeyboardEvent) {
        const code = e.code;
        const legacyCode = e.charCode || e.keyCode;

        if (
            (code === 'Space') || (code === 'Enter') ||
            (legacyCode === 32) || (legacyCode === 13) // space or enter
        ) {
            this.togglePopup();
        }
    }

    _onMapClick(e: MapMouseEvent) {
        const targetElement = e.originalEvent.target;
        const element = this._element;

        if (this._popup && (targetElement === element || element.contains(targetElement as any))) {
            this.togglePopup();
        }
    }

    /**
     * Returns the {@link Popup} instance that is bound to the {@link Marker}.
     * @returns {Popup} popup
     * @example
     * var marker = new maplibregl.Marker()
     *  .setLngLat([0, 0])
     *  .setPopup(new maplibregl.Popup().setHTML("<h1>Hello World!</h1>"))
     *  .addTo(map);
     *
     * console.log(marker.getPopup()); // return the popup instance
     */
    getPopup() {
        return this._popup;
    }

    /**
     * Opens or closes the {@link Popup} instance that is bound to the {@link Marker}, depending on the current state of the {@link Popup}.
     * @returns {Marker} `this`
     * @example
     * var marker = new maplibregl.Marker()
     *  .setLngLat([0, 0])
     *  .setPopup(new maplibregl.Popup().setHTML("<h1>Hello World!</h1>"))
     *  .addTo(map);
     *
     * marker.togglePopup(); // toggle popup open or closed
     */
    togglePopup() {
        const popup = this._popup;

        if (!popup) return this;
        else if (popup.isOpen()) popup.remove();
        else popup.addTo(this._map);
        return this;
    }

    _update(e?: {
        type: 'move' | 'moveend';
    }) {
        if (!this._map) return;

        if (this._map.transform.renderWorldCopies) {
            this._lngLat = smartWrap(this._lngLat, this._pos, this._map.transform);
        }

        this._pos = this._map.project(this._lngLat)._add(this._offset);

        let rotation = '';
        if (this._rotationAlignment === 'viewport' || this._rotationAlignment === 'auto') {
            rotation = `rotateZ(${this._rotation}deg)`;
        } else if (this._rotationAlignment === 'map') {
            rotation = `rotateZ(${this._rotation - this._map.getBearing()}deg)`;
        }

        let pitch = '';
        if (this._pitchAlignment === 'viewport' || this._pitchAlignment === 'auto') {
            pitch = 'rotateX(0deg)';
        } else if (this._pitchAlignment === 'map') {
            pitch = `rotateX(${this._map.getPitch()}deg)`;
        }

        // because rounding the coordinates at every `move` event causes stuttered zooming
        // we only round them when _update is called with `moveend` or when its called with
        // no arguments (when the Marker is initialized or Marker#setLngLat is invoked).
        if (!e || e.type === 'moveend') {
            this._pos = this._pos.round();
        }

        DOM.setTransform(this._element, `${anchorTranslate[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${pitch} ${rotation}`);

        // in case of 3D, ask the terrain coords-framebuffer for this pos and check if the marker is visible
        // call this logic in setTimeout with a timeout of 100ms to save performance in map-movement
        if (this._map.style && this._map.style.terrain && !this._opacityTimeout) this._opacityTimeout = setTimeout(() => {
            const lnglat = this._map.unproject(this._pos);
            const metresPerPixel = 40075016.686 * Math.abs(Math.cos(this._lngLat.lat * Math.PI / 180)) / Math.pow(2, this._map.transform.tileZoom + 8);
            this._element.style.opacity = lnglat.distanceTo(this._lngLat) > metresPerPixel * 20 ? '0.2' : '1.0';
            this._opacityTimeout = null;
        }, 100);
    }

    /**
     * Get the marker's offset.
     * @returns {Point} The marker's screen coordinates in pixels.
     */
    getOffset() {
        return this._offset;
    }

    /**
     * Sets the offset of the marker
     * @param {PointLike} offset The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up.
     * @returns {Marker} `this`
     */
    setOffset(offset: PointLike) {
        this._offset = Point.convert(offset);
        this._update();
        return this;
    }

    _onMove(e: MapMouseEvent | MapTouchEvent) {
        if (!this._isDragging) {
            const clickTolerance = this._clickTolerance || this._map._clickTolerance;
            this._isDragging = e.point.dist(this._pointerdownPos) >= clickTolerance;
        }
        if (!this._isDragging) return;

        this._pos = e.point.sub(this._positionDelta);
        this._lngLat = this._map.unproject(this._pos);
        this.setLngLat(this._lngLat);
        // suppress click event so that popups don't toggle on drag
        this._element.style.pointerEvents = 'none';

        // make sure dragstart only fires on the first move event after mousedown.
        // this can't be on mousedown because that event doesn't necessarily
        // imply that a drag is about to happen.
        if (this._state === 'pending') {
            this._state = 'active';

            /**
             * Fired when dragging starts
             *
             * @event dragstart
             * @memberof Marker
             * @instance
             * @type {Object}
             * @property {Marker} marker object that is being dragged
             */
            this.fire(new Event('dragstart'));
        }

        /**
         * Fired while dragging
         *
         * @event drag
         * @memberof Marker
         * @instance
         * @type {Object}
         * @property {Marker} marker object that is being dragged
         */
        this.fire(new Event('drag'));
    }

    _onUp() {
        // revert to normal pointer event handling
        this._element.style.pointerEvents = 'auto';
        this._positionDelta = null;
        this._pointerdownPos = null;
        this._isDragging = false;
        this._map.off('mousemove', this._onMove);
        this._map.off('touchmove', this._onMove);

        // only fire dragend if it was preceded by at least one drag event
        if (this._state === 'active') {
            /**
             * Fired when the marker is finished being dragged
             *
             * @event dragend
             * @memberof Marker
             * @instance
             * @type {Object}
             * @property {Marker} marker object that was dragged
             */
            this.fire(new Event('dragend'));
        }

        this._state = 'inactive';
    }

    _addDragHandler(e: MapMouseEvent | MapTouchEvent) {
        if (this._element.contains(e.originalEvent.target as any)) {
            e.preventDefault();

            // We need to calculate the pixel distance between the click point
            // and the marker position, with the offset accounted for. Then we
            // can subtract this distance from the mousemove event's position
            // to calculate the new marker position.
            // If we don't do this, the marker 'jumps' to the click position
            // creating a jarring UX effect.
            this._positionDelta = e.point.sub(this._pos).add(this._offset);

            this._pointerdownPos = e.point;

            this._state = 'pending';
            this._map.on('mousemove', this._onMove);
            this._map.on('touchmove', this._onMove);
            this._map.once('mouseup', this._onUp);
            this._map.once('touchend', this._onUp);
        }
    }

    /**
     * Sets the `draggable` property and functionality of the marker
     * @param {boolean} [shouldBeDraggable=false] Turns drag functionality on/off
     * @returns {Marker} `this`
     */
    setDraggable(shouldBeDraggable: boolean) {
        this._draggable = !!shouldBeDraggable; // convert possible undefined value to false

        // handle case where map may not exist yet
        // e.g. when setDraggable is called before addTo
        if (this._map) {
            if (shouldBeDraggable) {
                this._map.on('mousedown', this._addDragHandler);
                this._map.on('touchstart', this._addDragHandler);
            } else {
                this._map.off('mousedown', this._addDragHandler);
                this._map.off('touchstart', this._addDragHandler);
            }
        }

        return this;
    }

    /**
     * Returns true if the marker can be dragged
     * @returns {boolean} True if the marker is draggable.
     */
    isDraggable() {
        return this._draggable;
    }

    /**
     * Sets the `rotation` property of the marker.
     * @param {number} [rotation=0] The rotation angle of the marker (clockwise, in degrees), relative to its respective {@link Marker#setRotationAlignment} setting.
     * @returns {Marker} `this`
     */
    setRotation(rotation: number) {
        this._rotation = rotation || 0;
        this._update();
        return this;
    }

    /**
     * Returns the current rotation angle of the marker (in degrees).
     * @returns {number} The current rotation angle of the marker.
     */
    getRotation() {
        return this._rotation;
    }

    /**
     * Sets the `rotationAlignment` property of the marker.
     * @param {string} [alignment='auto'] Sets the `rotationAlignment` property of the marker.
     * @returns {Marker} `this`
     */
    setRotationAlignment(alignment: string) {
        this._rotationAlignment = alignment || 'auto';
        this._update();
        return this;
    }

    /**
     * Returns the current `rotationAlignment` property of the marker.
     * @returns {string} The current rotational alignment of the marker.
     */
    getRotationAlignment() {
        return this._rotationAlignment;
    }

    /**
     * Sets the `pitchAlignment` property of the marker.
     * @param {string} [alignment] Sets the `pitchAlignment` property of the marker. If alignment is 'auto', it will automatically match `rotationAlignment`.
     * @returns {Marker} `this`
     */
    setPitchAlignment(alignment: string) {
        this._pitchAlignment = alignment && alignment !== 'auto' ? alignment : this._rotationAlignment;
        this._update();
        return this;
    }

    /**
     * Returns the current `pitchAlignment` property of the marker.
     * @returns {string} The current pitch alignment of the marker in degrees.
     */
    getPitchAlignment() {
        return this._pitchAlignment;
    }
}
