import { JsonImporter } from './importer/json-importer';
import { ApiImporter } from './importer/api-importer';
import { stripMultilineIndention } from '../../common/string-utilities';

/**
 * @typedef {object} FilteredMarkerSource~Options
 * @property {object[]} importers
 * @property {string} importers[].type
 */

export class FilteredMarkerSource {

    /**
     * @param {FilteredMarkerSource~Options} options
     */
    constructor (options) {

        /** @private {Object<string, function[]>} */
        this.listeners = {
            currentFilter: [],
            allFilterItems: [],
            allMarkers: [],
            filteredMarkers: [],
        };

        /** @private */
        this.data = {
            currentFilter: null,
            allFilterItems: [],
            allMarkers: [],
            filteredMarkers: [],
        };

        /** @private {FilteredMarkerSource~Options} */
        this.options = options;
        this.options.importers = this.options.importers || [];
        this.options.defaultMarkerStyle = this.options.defaultMarkerStyle || {};

        this.updateFilteredMarkersOnDataChanges();
    }

    /**
     * @public
     * @param {function} listener
     */
    currentFilter(listener) {
        this.listeners.currentFilter.push(listener);
        listener(this.data.currentFilter);
    }

    /**
     * @public
     * @param {function} listener
     */
    allFilterItems(listener) {
        this.listeners.allFilterItems.push(listener);
        listener(this.data.allFilterItems);
    }

    /**
     * @public
     * @param {function} listener
     */
    allMarkers(listener) {
        this.listeners.allMarkers.push(listener);
        listener(this.data.allMarkers);
    }

    /**
     * @public
     * @param {function} listener
     */
    filteredMarkers(listener) {
        this.listeners.filteredMarkers.push(listener);
        listener(this.data.filteredMarkers);
    }

    /**
     * @private
     * @param {string} type
     */
    callListeners(type) {
        for (const listener of this.listeners[type]) {
            listener(this.data[type]);
        }
    }

    /**
     * @public
     * @param {FilterItem} filter
     */
    setCurrentFilter (filter) {
        if (filter !== this.data.currentFilter) {
            this.data.currentFilter = filter;
            this.callListeners('currentFilter');
        }
    }

    /**
     * @public
     * @param {Marker} marker
     */
    addMarker (marker) {
        if (!this.isMarkerValid(marker)) {
            return;
        }
        this.data.allMarkers.push(marker);
        this.callListeners('allMarkers');
    }

    /**
     * @public
     * @param {Marker[]} markers
     */
    addMarkers(markers) {
        for (const marker of markers) {
            if (!this.isMarkerValid(marker)) {
                continue;
            }
            this.data.allMarkers.push(marker);
        }
        this.callListeners('allMarkers');
    }

    /**
     * @private
     * @param {Marker} marker
     * @returns {boolean}
     */
    isMarkerValid(marker) {
        if (!marker.coordinates || marker.coordinates.latitude === 0 || marker.coordinates.longitude === 0) {
            console.warn(stripMultilineIndention(`
                ## Toubiz Map
                Not importing a map marker without coordiantes:
            `), marker);
            return false;
        }
        if (
            marker.coordinates.latitude > 90
            || marker.coordinates.latitude < -90
            || marker.coordinates.longitude > 180
            || marker.coordinates.longitude < -180
        ) {
            console.warn(stripMultilineIndention(`
                ## Toubiz Map
                Coordinates of marker are not within acceptable bounds. Not importing.
            `), marker);
            return false;
        }

        if (this.data.allMarkers.indexOf(marker) !== -1) {
            console.warn(stripMultilineIndention(`
                ## Toubiz Map
                Attempting to add the same marker twice.
            `), marker);
            return false;
        }

        return true;
    }

    /**
     * @public
     * @param {FilterItem} filterItem
     */
    addFilterItem (filterItem) {
        this.data.allFilterItems.push(filterItem);
        this.callListeners('allFilterItems');

        const currentFilterItem = this.findOpenFilterItem([ filterItem ]);
        if (currentFilterItem !== null) {
            this.setCurrentFilter(currentFilterItem);
        }
    }
    /**
     * @public
     * @param {FilterItem[]} filterItems
     */
    addFilterItems (filterItems) {
        for (const filterItem of filterItems) {
            this.data.allFilterItems.push(filterItem);
        }

        const currentFilterItem = this.findOpenFilterItem(filterItems);
        if (currentFilterItem !== null) {
            this.setCurrentFilter(currentFilterItem);
        }

        this.callListeners('allFilterItems');
    }

    /**
     * @private
     * @param {FilterItem[]} filterItems
     * @return {FilterItem|null}
     */
    findOpenFilterItem(filterItems) {
        const flatItems = this.flattenFilterItems(filterItems);

        return flatItems.find(item => item.forceOpen)
            || flatItems.find(item => item.openByDefault)
            || null;
    }

    /**
     * @private
     * @param {FilterItem[]} filterItems
     */
    flattenFilterItems(filterItems) {
        const flatItems = [];
        const queue = [ ...filterItems ];

        let item;
        while (item = queue.shift()) {
            flatItems.push(item);
            for (const child of item.children) {
                queue.push(child);
            }

        }

        return flatItems;
    }

    /** @private */
    updateFilteredMarkersOnDataChanges() {
        const update = (markers, filter) => {
            this.data.filteredMarkers = this.filterMarkers(markers, filter);
            this.callListeners('filteredMarkers');
        };

        this.allMarkers(markers => update(markers, this.data.currentFilter));
        this.allFilterItems(() => update(this.data.allMarkers, this.data.currentFilter));
        this.currentFilter(filter => update(this.data.allMarkers, filter));
    }


    /** @returns {Promise<Marker[]>} */
    getFilteredMarkers() {
        return Promise.resolve(this.data.filteredMarkers);
    }

    /**
     * @private
     * @param {Marker[]} markers
     * @param {FilterItem} filter
     * @returns {Marker[]}
     */
    filterMarkers(markers, filter) {
        // Remove markers with invalid coordinates
        markers = markers.filter(marker => marker.coordinates
                && marker.coordinates.latitude
                && marker.coordinates.longitude);

        // Apply the given filter
        const path = filter ? filter.path : '/';
        markers = markers.filter(marker => this.isMarkerInPath(marker, path));

        // Apply correct styles
        markers = markers.map(marker => {
            marker.style = this.getMarkerStyle(marker, filter);
            return marker;
        });

        return markers;
    }

    /**
     * Parses the markup for static markers and filter (sub)categories
     *
     * @returns {Promise}
     */
    async importData () {
        const constructors = {
            json: JsonImporter,
            api: ApiImporter,
        };

        const importers = this.options.importers
            .map(config => {
                if (!constructors[ config.type ]) {
                    console.error(`No importer of type '${config.type}' available.`, config);
                    return null;
                }
                return new constructors[ config.type ](this, config);
            })
            .filter(importer => !!importer);

        const promises = importers.map(importer => importer.import());
        await Promise.all(promises);

        // Actually wait for filter processing to finish
        await this.getFilteredMarkers();
    }

    /**
     * @private
     * @param {Marker} marker
     * @param {FilterItem} filterItem
     * @returns {Marker~Style}
     */
    getMarkerStyle (marker, filterItem) {
        // Fallback: If no filter is selected then the top level filter item style is used
        if (filterItem === null || filterItem.path === '/') {
            filterItem = this.getTopLevelFilterItem(marker);
        }

        // Choose the right style to be displayed
        let style;
        if (filterItem && filterItem.overwriteMarkerStyles) {
            style = Object.assign({}, filterItem.style);
        } else if (marker.defaultStyle) {
            style = Object.assign({}, marker.defaultStyle);
        } else {
            style = {};
        }

        if (marker.highlightIcon) {
            style.iconUrl = marker.highlightIcon;
        }

        // Ensure that all properties are set
        if (!style.hasAllProperties) {
            this.applyDefaultMarkerStyle(style);
            style.hasAllProperties = true;
        }

        return style;
    }

    /**
     * @private
     * @param {Marker~Style} style
     */
    applyDefaultMarkerStyle(style) {
        style.iconUrl = style.iconUrl || this.options.defaultMarkerStyle.iconUrl;
        style.width = style.width || this.options.defaultMarkerStyle.width;
        style.height = style.height || this.options.defaultMarkerStyle.height;
        style.offsetX = style.offsetX || this.options.defaultMarkerStyle.offsetX;
        style.offsetY = style.offsetY || this.options.defaultMarkerStyle.offsetY;
    }

    /**
     * @private
     * @param {Marker} marker
     * @returns {FilterItem|null}
     */
    getTopLevelFilterItem (marker) {
        for (const item of this.data.allFilterItems) {
            if (this.isMarkerInPath(marker, item.path)) {
                return item;
            }
        }
        return null;
    }

    isMarkerInPath(marker, path) {
        for (const markerPath of marker.paths) {
            if (markerPath.indexOf(path) === 0) {
                return true;
            }
        }
        return false;
    }
}
