/**
 * @typedef {object} AbstractMapWithMarkers~Options
 * @property {Marker~Coordinates} center
 * @property {number} zoom - Zoom level from 0 to 100
 * @property {object} clustering
 * @property {string} clustering.color
 * @property {string} clustering.backgroundColor
 * @property {string} clustering.spiderficationIcon
 */

/**
 * Abstract class that must be extended for every map implementation.
 * It provides a bit of common scaffolding such as handling of marker
 * updates.
 *
 * The most important method that must be implemented by subclasses is
 * the `render` method which is called every time something about the map
 * changes and it should be updated.
 *
 * @abstract
 */
export class AbstractMapWithMarkers {

    /**
     * @param {HTMLElement} node
     * @param {AbstractMapWithMarkers~Options} options
     * @param {Observable<Marker[]>} markerProvider
     */
    constructor (node, options) {

        /** @protected {HTMLElement} node */
        this.node = node;

        /** @protected {AbstractMapWithMarkers~Options} */
        this.options = this.prepareOptions(options);

        /** @protected {object<string, Function[]>} */
        this.listeners = { markerClick: []};
    }

    /**
     * Method that is called after basic instance variables are set but
     * before the first render.
     *
     * @public
     * @returns {void}
     */
    async initialize() {
        // Can be called if needed
    }

    onMarkerClick(callback) {
        this.listeners.markerClick.push(callback);
    }

    callMarkerClickListeners(marker) {
        this.listeners.markerClick.forEach(cb => cb(marker));
    }

    /**
     * Main rendering method for maps that is called every time something about the
     * map changes. This method should return a promise (or be async) if the rendering
     * is done asynchronosly.
     *
     * Additionally, it is recommended to use a lock in order to prevent race conditions
     * when rendering in short sucession.
     *
     * @protected
     * @abstract
     * @returns {Promise|void}
     */
    // eslint-disable-next-line no-unused-vars
    render(markers = []) {
        throw new Error('This method must be implemented by subclasses.');
    }


    /**
     * Centers the current map on all shown markers.
     * Implement this method if your map supports moving around - don't implement it if it doesn't.
     *
     * @public
     * @param {Marker[]|null} markers
     * @returns {Promise|void}
     */
    /* eslint-disable-next-line no-unused-vars */
    centerMapOnMarkers(markers) {}

    /**
     * Prepares the options for usage. This method sets default options or transforms the
     * incoming options to the correct format.
     *
     * Implement this method in your map implementation if there are specific options for your
     * map that have default values. Don't forget to include a `super` call though.
     *
     * @param {object} options
     * @returns {object}
     * @protected
     */
    prepareOptions(options) {
        options.center = options.center || { latitude: 47.920130, longitude: 7.705250 };
        options.zoom = options.zoom || 80;
        options.clustering = options.clustering || {};
        options.clustering.color = options.clustering.color || 'white';
        options.clustering.backgroundColor = options.clustering.backgroundColor || '#333';

        return options;
    }

    /**
     * @public
     * @param {number} latitude
     * @param {number} longitude
     * @returns {Promise}
     */
    async setCenter(latitude, longitude) {
        this.options.center = { latitude, longitude };
        await this.render();
    }

    /**
     * @public
     * @param {number} zoom
     * @returns {Promise}
     */
    async setZoom(zoom) {
        this.options.zoom = zoom;
        await this.render();
    }

}
