/**
 * Map util functions
 */

define([
    'jquery',
    'app/init',
    'app/util',
    'app/map/layout',
    'app/map/scrollbar',
    'app/map/overlay/util'
], ($, Init, Util, Layout, Scrollbar, MapOverlayUtil) => {
    'use strict';

    let config = {
        mapSnapToGridDimension: 20,                                     // px for grid snapping (grid YxY)
        defaultLocalJumpRadius: 3,                                      // default search radius (in jumps) for "nearby" pilots
        zoomMax: 1.5,
        zoomMin: 0.5,

        mapGridClass: 'pf-grid-small',                                  // class for map grid snapping
        systemRegionClass: 'pf-map-region',                             // class for systems regions on map
        systemCompactClass: 'pf-map-compact',                           // class for compact systems on map

        systemIdPrefix: 'pf-system-',                                   // id prefix for a system
        systemClass: 'pf-system',                                       // class for all systems
        systemActiveClass: 'pf-system-active',                          // class for an active system on a map
        systemSelectedClass: 'pf-system-selected',                      // class for selected systems on on map
        systemLockedClass: 'pf-system-locked',                          // class for locked systems on a map
        systemHiddenClass: 'pf-system-hidden',                          // class for hidden (filtered) systems

        // dataTable
        tableCellEllipsisClass: 'pf-table-cell-ellipsis',
        tableCellEllipsis80Class: 'pf-table-cell-80',
        tableCellEllipsis90Class: 'pf-table-cell-90',
        tableCellEllipsis100Class: 'pf-table-cell-100'
    };

    // active jsPlumb instances currently running =====================================================================
    let activeInstances = {};

    /**
     * set mapInstance
     * @param mapId
     * @param map
     */
    let setMapInstance = (mapId, map) => {
        activeInstances[mapId] = map;
    };

    /**
     * get mapInstance
     * @param mapId
     * @returns {*}
     */
    let getMapInstance = mapId => activeInstances[mapId];

    /**
     * check for mapInstance is set
     * @param mapId
     * @returns {boolean}
     */
    let existsMapInstance = mapId => typeof activeInstances[mapId] === 'object';

    /**
     * removes a map instance
     * @param mapId
     */
    let clearMapInstance = mapId => {
        if(existsMapInstance(mapId)){
            delete activeInstances[mapId];
        }
    };

    // ================================================================================================================

    /**
     * get all available map Types
     * optional they can be filtered by current access level of current character
     * @param {bool} filterByCharacter
     * @param {string} filterRight
     * @returns {Array}
     */
    let getMapTypes = (filterByCharacter, filterRight) => {
        let mapTypes = Object.assign({}, Init.mapTypes);

        if(filterByCharacter === true){
            let authorizedMapTypes = [];
            let checkMapTypes = [
                {type: 'private',       hasRight: false, selector: 'id'},
                {type: 'corporation',   hasRight: true,  selector: 'corporation.id'},
                {type: 'alliance',      hasRight: false,  selector: 'alliance.id'}
            ];

            checkMapTypes.forEach(data => {
                // check if current character is e.g. in alliance
                if(Util.getCurrentCharacterData(data.selector)){
                    // check if User could add new map with a mapType -> check map limit
                    let currentObjectMapData = Util.filterCurrentMapData('config.type.id', Util.getObjVal(mapTypes, data.selector));
                    let maxCountObject = Util.getObjVal(mapTypes, `${data.type}.defaultConfig.max_count`);
                    if(currentObjectMapData.length < maxCountObject){
                        // check if character has the "right" for creating a map with this type
                        if((data.hasRight && filterRight) ? Util.hasRight(filterRight, data.type) : true){
                            authorizedMapTypes.push(data.type);
                        }
                    }
                }
            });

            mapTypes = Util.filterObjByKeys(mapTypes, authorizedMapTypes);
        }

        // add "name" to mapType data
        Object.entries(mapTypes).forEach(([mapType, data]) => {
            data.name = mapType;
        });

        // obj to array
        return Object.keys(mapTypes).map(mapType => mapTypes[mapType]);
    };

    /**
     * get all available scopes for a map
     * @returns {Array}
     */
    let getMapScopes = () => {
        let scopes = [];
        $.each(Init.mapScopes, function(prop, data){
            let tempData = data;
            tempData.name = prop;
            scopes.push(tempData);
        });

        return scopes;
    };

    /**
     * get some scope info for a given info string
     * @param {string} info
     * @param {string} option
     * @returns {string}
     */
    let getScopeInfoForMap = (info, option) => {
        let scopeInfo = '';
        if(Init.mapScopes.hasOwnProperty(info)){
            scopeInfo = Init.mapScopes[info][option];
        }
        return scopeInfo;
    };

    /**
     * get all available map icons
     * @returns {Object[]}
     */
    let getMapIcons = () => {
        return Init.mapIcons;
    };

    /**
     * get map info
     * @param {string} mapType
     * @param {string} option
     * @returns {string}
     */
    let getInfoForMap = (mapType, option) => {
        let mapInfo = '';
        if(Init.mapTypes.hasOwnProperty(mapType)){
            mapInfo = Init.mapTypes[mapType][option];
        }
        return mapInfo;
    };

    /**
     * get some system info for a given info string (e.g. rally class)
     * @param {string} info
     * @param {string} option
     * @returns {string}
     */
    let getInfoForSystem = (info, option) => {
        let systemInfo = '';
        if(Init.classes.systemInfo.hasOwnProperty(info)){
            systemInfo = Init.classes.systemInfo[info][option];
        }
        return systemInfo;
    };

    /**
     * get system data from mapData
     * @see getSystemData
     * @param mapData
     * @param value
     * @param key
     * @returns {{}|boolean}
     */
    let getSystemDataFromMapData = (mapData, value, key = 'id') => {
        return (Util.getObjVal(mapData, `data.systems`) || [])
            .find(systemData => systemData[key] === value) || false;
    };

    /**
     * get system data by mapId + system data selector
     * -> e.g. value = 2 and key = 'id'
     * -> e.g. value = 30002187 and key = 'systemId' => looks for 'Amarr' CCP systemId
     * @param mapId
     * @param value
     * @param key
     * @returns {{}|boolean}
     */
    let getSystemData = (mapId, value, key = 'id') => {
        return getSystemDataFromMapData(Util.getCurrentMapData(mapId), value, key);
    };

    /**
     * get connection data from mapData
     * @see getConnectionData
     * @param mapData
     * @param value
     * @param key
     * @returns {{}|boolean}
     */
    let getConnectionDataFromMapData = (mapData, value, key = 'id') => {
        return (Util.getObjVal(mapData, `data.connections`) || [])
            .find(connectionData => connectionData[key] === value) || false;
    };

    /**
     * get connection data by mapId + connection data selector
     * @param mapId
     * @param value
     * @param key
     * @returns {{}|boolean}
     */
    let getConnectionData = (mapId, value, key = 'id') => {
        return getConnectionDataFromMapData(Util.getCurrentMapData(mapId), value, key);
    };

    /**
     * get system type information
     * @param {number} systemTypeId
     * @param {string} option
     * @returns {string}
     */
    let getSystemTypeInfo = (systemTypeId, option) => {
        return (Object.values(Init.systemType).find(data => data.id === systemTypeId) || {})[option] || '';
    };

    /**
     * get some info for a given effect string
     * @param effect
     * @param option
     * @returns {string}
     */
    let getEffectInfoForSystem = (effect, option) => {
        return Util.getObjVal(Init.classes.systemEffects, `${effect}.${option}`) || '';
    };

    /**
     * flag map component (map, system, connection) as "changed"
     * @param component
     */
    let markAsChanged = component => {
        if(component instanceof $ && component.hasClass(config.systemClass)){
            component.data('changed', true);
        }else if(component instanceof jsPlumb.Connection){
            component.setParameter('changed', true);
        }
    };

    /**
     * check if map component (system, connection) is flagged as "changed"
     * @param component
     * @returns {boolean}
     */
    let hasChanged = component => {
        let changed = false;
        if(component instanceof $ && component.hasClass(config.systemClass)){
            changed = component.data('changed') || false;
        }else if(component instanceof jsPlumb.Connection){
            changed = component.getParameter('changed') || false;
        }
        return changed;
    };

    /**
     * get system elements on a map
     * @returns {*|jQuery}
     */
    $.fn.getSystems = function(){
        return this.find('.' + config.systemClass);
    };

    /**
     * get all selected (NOT active) systems on a map
     * @returns {*}
     */
    $.fn.getSelectedSystems = function(){
        let mapElement = $(this);
        return mapElement.find('.' + config.systemSelectedClass);
    };

    /**
     * filter jsPlumb connection or endpoint types
     * -> remove default type(s)
     * @param types
     * @returns {*}
     */
    let filterDefaultTypes = types => {
        let defaultTypes = ['', 'default', 'info_signature', 'state_active', 'state_process'];
        return types.diff(defaultTypes);
    };

    /**
     * returns "target/source"  label from endpoint
     * @param connection
     * @param endpoint
     * @returns {string}
     */
    let getEndpointLabel = (connection, endpoint) => {
        return endpoint.element === connection.source ? 'source' : endpoint.element === connection.target ? 'target' : false;
    };

    /**
     * get data from endpoint
     * @param connection
     * @param endpoint
     * @returns {{types: *, label: string}}
     */
    let getDataByEndpoint = (connection, endpoint) => {
        return {
            label: getEndpointLabel(connection, endpoint),
            types: filterDefaultTypes(endpoint.getType())
        };
    };

    /**
     * filter connections by type
     * @param map
     * @param type
     * @param exclude
     * @returns {Array}
     */
    let getConnectionsByType = (map, type, exclude = false) => {
        let connections = [];
        for(let data of map.select().hasType(type)){
            if(data[0] !== exclude){
                connections.push(data[1]);
            }
        }
        return connections;
    };

    /**
     * get endpoints data from connection
     * @param connection
     * @returns {{source: {}, target: {}}}
     */
    let getEndpointsDataByConnection = connection => {
        let endpointsData = {source: {}, target: {}};
        for(let endpoint of connection.endpoints){
            let endpointData = getDataByEndpoint(connection, endpoint);
            if(endpointData.label === 'source'){
                endpointsData.source = endpointData;
            }else if(endpointData.label === 'target'){
                endpointsData.target = endpointData;
            }
        }
        return endpointsData;
    };

    /**
     * get connection data from connection
     * @param connection
     * @param minimal
     * @returns {{id: (*|number), updated: (*|number)}}
     */
    let getDataByConnection = (connection, minimal = false) => {
        let source = $(connection.source);
        let target = $(connection.target);

        let connectionData = {
            id: connection.getParameter('connectionId') || 0,
            updated: connection.getParameter('updated') || 0,
            source: parseInt(source.data('id')),
            target: parseInt(target.data('id'))
        };

        if(minimal){
            connectionData = Object.assign(connectionData, {
                connection: connection
            });
        }else{
            connectionData = Object.assign(connectionData, {
                sourceName: source.data('name'),
                sourceAlias: source.getSystemInfo(['alias']) || source.data('name'),
                targetName: target.data('name'),
                targetAlias: target.getSystemInfo(['alias']) || target.data('name'),
                scope: connection.scope,
                type: filterDefaultTypes(connection.getType()),
                endpoints: getEndpointsDataByConnection(connection)
            });
        }

        return connectionData;
    };

    /**
     * @see getDataByConnection
     * @param connections
     * @returns {Array}
     */
    let getDataByConnections = connections => {
        let data = [];
        for(let connection of connections){
            data.push(getDataByConnection(connection));
        }
        return data;
    };

    /**
     * delete a connection and all related data
     * @param connections
     * @param callback
     */
    let deleteConnections = (connections, callback) => {
        if(connections.length > 0){

            // remove connections from map
            let removeConnections = connections => {
                for(let connection of connections){
                    connection._jsPlumb.instance.deleteConnection(connection, {fireEvent: false});
                }
            };

            // prepare delete request
            let map = connections[0]._jsPlumb.instance;
            let mapContainer = $(map.getContainer());

            // connectionIds for delete request
            let connectionIds = [];
            for(let connection of connections){
                connection.addType('state_process');

                let connectionId = connection.getParameter('connectionId');
                // drag&drop a new connection does not have an id yet, if connection is not established correct
                if(connectionId !== undefined){
                    connectionIds.push(connectionId);
                }
            }

            if(connectionIds.length > 0){
                Util.request('DELETE', 'Connection', connectionIds, {
                    mapId: mapContainer.data('id')
                }, {
                    connections: connections
                }).then(
                    payload => {
                        for(let connection of payload.context.connections){
                            // connection might be removed rom global map update before this requests ends
                            if(connection._jsPlumb){
                                connection.removeType('state_process');
                            }
                        }

                        // check if all connections were deleted that should get deleted
                        let deletedConnections = payload.context.connections.filter(
                            function(connection){
                                // if a connection is manually (drag&drop) detached, the jsPlumb instance does not exist any more
                                // connection is already deleted!
                                return (
                                    connection._jsPlumb &&
                                    this.indexOf( connection.getParameter('connectionId') ) !== -1
                                );
                            }, payload.data
                        );

                        // remove connections from map
                        removeConnections(deletedConnections);

                        if(callback){
                            callback();
                        }
                    },
                    Util.handleAjaxErrorResponse
                );
            }
        }
    };

    /**
     * get connection related data from a connection
     * -> data requires a signature bind to that connection
     * @param connection
     * @param connectionData
     * @returns {{source: {names: Array, labels: Array}, target: {names: Array, labels: Array}}}
     */
    let getConnectionDataFromSignatures = (connection, connectionData) => {
        let signatureTypeData = {
            source: {
                ids: [],
                names: [],
                labels: []
            },
            target: {
                ids: [],
                names: [],
                labels: []
            },
            hash: false     // unique hash key build from all relevant signature for connection
        };

        if(
            connection &&
            connectionData &&
            connectionData.signatures   // signature data is required...
        ){
            let SystemSignatureModule = require('module/system_signature');

            let sourceEndpoint      = connection.endpoints[0];
            let targetEndpoint      = connection.endpoints[1];
            let sourceSystem        = $(sourceEndpoint.element);
            let targetSystem        = $(targetEndpoint.element);
            let sourceId            = sourceSystem.data('id');
            let targetId            = targetSystem.data('id');

            // in case connection is currently "dragged" between systems, sourceId and/or targetId is undefined
            if(!sourceId || !targetId){
                return signatureTypeData;
            }

            let hash = [];
            // ... collect overlay/label data from signatures
            for(let signatureData of connectionData.signatures){
                hash.push(Util.getObjVal(signatureData, 'updated.updated'));

                // whether "source" or "target" system is relevant for current connection and current signature...
                let tmpSystem = null;
                let tmpSystemType = null;

                if(signatureData.system.id === sourceId){
                    // relates to "source" endpoint
                    tmpSystemType = 'source';
                    tmpSystem = sourceSystem;
                }else if(signatureData.system.id === targetId){
                    // relates to "target" endpoint
                    tmpSystemType = 'target';
                    tmpSystem = targetSystem;
                }

                signatureTypeData[tmpSystemType].ids.push(signatureData.id);
                signatureTypeData[tmpSystemType].names.push(signatureData.name.toUpperCase());

                // ... typeId is required to get a valid labels
                // ... get endpoint label for source || target system
                if(signatureData.typeId > 0 && tmpSystem && tmpSystem){
                    // ... get all  available signature type (wormholes) names
                    let availableSigTypeNames = SystemSignatureModule.getSignatureTypeOptionsBySystem(tmpSystem, 5);
                    let flattenSigTypeNames = Util.flattenXEditableSelectArray(availableSigTypeNames);

                    if(flattenSigTypeNames.hasOwnProperty(signatureData.typeId)){
                        let label = flattenSigTypeNames[signatureData.typeId];
                        // shorten label, just take the ingame name
                        label = label.substr(0, label.indexOf(' '));
                        signatureTypeData[tmpSystemType].labels.push(label);
                    }
                }
            }
            // ... build unique hash
            signatureTypeData.hash = hash.join().hashCode();
        }

        return signatureTypeData;
    };

    /**
     * get Location [x,y] for Endpoint Overlays (e.g. wh type from Signature mapping)
     * -> Coordinates are relative to the Endpoint (not the system!)
     * -> jsPlumb specific format
     * @param endpoint
     * @param labels
     * @returns {number[]}
     */
    let getEndpointOverlaySignatureLocation = (endpoint, labels) => {
        let defaultLocation = [0.5, 0.5];

        if(endpoint.anchor.getCurrentFace){
            // ContinuousAnchor 
            let count   = labels.length;
            let xLeft   = count ? count === 1 ? -1.00 : 3 : -0.5;
            let xRight  = count ? count === 1 ? +2.20 : 3 : +1.5;

            switch(endpoint.anchor.getCurrentFace()){
                case 'top':     return [0.5, -0.75];
                case 'left':    return [xLeft, 0.25];
                case 'right':   return [xRight, 0.25];
                case 'bottom':  return [0.5 , 1.75];
                default:        return defaultLocation;
            }
        }else{
            // e.g. floating endpoint (dragging)
            // -> ContinuousAnchor
            return defaultLocation;
        }
    };

    /**
     * get overlay HTML for connection endpoints by Label array
     * @param labels
     * @returns {string}
     */
    let formatEndpointOverlaySignatureLabel = labels => {
        // default K162 in label array, or multiple labels
        let colorClass = 'txt-color-grayLighter';
        let label = labels.join(', ');

        if(labels.length === 0){
            // endpoint not connected with a signature
            label = '<i class="fas fa-question-circle"></i>';
            colorClass = 'txt-color-red';
        }else if(
            labels.length === 1 &&
            !labels.includes('K162')
        ){
            colorClass = Init.wormholes[labels[0]].class;
        }

        return '<span class="txt-color ' + colorClass + '">' + label + '</span>';
    };

    /**
     * get TabContentElement by any element on a map e.g. system
     * @param element
     * @returns {*}
     */
    let getTabContentElementByMapElement = element => $(element).closest('.' + Util.config.mapTabContentClass);

    /**
     * checks if there is an "active" connection on a map
     * @param map
     * @returns {boolean}
     */
    let hasActiveConnection = map => {
        let activeConnections = getConnectionsByType(map, 'state_active');
        return activeConnections.length > 0;
    };

    /**
     * filter map by scopes
     * -> this effects visible systems and connections on UI
     * @param map
     * @param scopes
     */
    let filterMapByScopes = (map, scopes) => {
        if(map){
            map.setSuspendDrawing(true);

            let mapElement = $(map.getContainer());
            let allSystems = mapElement.getSystems();
            let allConnections = map.getAllConnections();

            if(scopes && scopes.length){
                // filter connections -------------------------------------------------------------------------------------
                let visibleSystems = [];
                let visibleConnections = searchConnectionsByScopeAndType(map, scopes);

                for(let connection of allConnections){
                    if(visibleConnections.indexOf(connection) >= 0){
                        setConnectionVisible(connection, true);
                        // source/target system should always be visible -> even if filter scope not matches system type
                        if(visibleSystems.indexOf(connection.endpoints[0].element) < 0){
                            visibleSystems.push(connection.endpoints[0].element);
                        }
                        if(visibleSystems.indexOf(connection.endpoints[1].element) < 0){
                            visibleSystems.push(connection.endpoints[1].element);
                        }
                    }else{
                        setConnectionVisible(connection, false);
                    }
                }

                // filter systems -----------------------------------------------------------------------------------------
                let visibleTypeIds = [];
                if(scopes.indexOf('wh') >= 0){
                    visibleTypeIds.push(1);
                }
                if(scopes.indexOf('abyssal') >= 0){
                    visibleTypeIds.push(4);
                }

                for(let system of allSystems){
                    if(
                        visibleTypeIds.indexOf($(system).data('typeId')) >= 0 ||
                        visibleSystems.indexOf(system) >= 0
                    ){
                        setSystemVisible(system, map, true);
                    }else{
                        setSystemVisible(system, map, false);
                    }
                }

                MapOverlayUtil.getMapOverlay(mapElement, 'info').updateOverlayIcon('filter', 'show');
            }else{
                // clear filter
                for(let system of allSystems){
                    setSystemVisible(system, map, true);
                }
                for(let connection of allConnections){
                    setConnectionVisible(connection, true);
                }

                MapOverlayUtil.getMapOverlay(mapElement, 'info').updateOverlayIcon('filter', 'hide');
            }

            map.setSuspendDrawing(false, true);
        }
    };

    /**
     * in/de-crease zoom level
     * @param map
     * @param zoomAction
     * @returns {boolean}
     */
    let changeZoom = (map, zoomAction) => {
        let zoomChange = false;
        let zoom = map.getZoom();
        let zoomStep = 0.1;
        if('up' === zoomAction){
            zoom += zoomStep;
        }else{
            zoom -= zoomStep;
        }
        zoom = Math.round(zoom * 10) / 10;
        if(zoom >= config.zoomMin && zoom <= config.zoomMax){
            zoomChange = setZoom(map, zoom);
        }
        return zoomChange;
    };

    /**
     * set zoom level for a map
     * @param map
     * @param zoom
     * @returns {boolean}
     */
    let setZoom = (map, zoom = 1) => {
        let zoomChange = false;
        if(zoom !== map.getZoom()){
            // zoom jsPlumb map http://jsplumb.github.io/jsplumb/zooming.html
            let transformOrigin = [0, 0];
            let el = map.getContainer();
            let p = ['webkit', 'moz', 'ms', 'o'];
            let s = 'scale(' + zoom + ')';
            let oString = (transformOrigin[0] * 100) + '% ' + (transformOrigin[1] * 100) + '%';

            for(let i = 0; i < p.length; i++){
                el.style[p[i] + 'Transform'] = s;
                el.style[p[i] + 'TransformOrigin'] = oString;
            }
            el.style.transform = s;
            el.style.transformOrigin = oString;

            zoomChange = map.setZoom(zoom);

            // adjust mCustomScrollbar --------------------------------------------------------------------------------
            let scaledWidth = el.getBoundingClientRect().width;
            let scaledHeight = el.getBoundingClientRect().height;
            let mapContainer = $(el);
            let mapWidth = mapContainer.outerWidth(); // this is fix (should never change)
            let mapHeight = mapContainer.outerHeight(); // this is fix (should never change)
            let wrapperWidth = mapContainer.parents('.mCSB_container_wrapper').outerWidth(); // changes on browser resize (map window)
            let wrapperHeight = mapContainer.parents('.mCSB_container_wrapper').outerHeight(); // changes on drag resize (map window)
            let scrollableWidth = (zoom === 1 || mapWidth !== scaledWidth && scaledWidth > wrapperWidth);
            let scrollableHeight = (zoom === 1 || mapHeight !== scaledHeight && scaledHeight > wrapperHeight);

            mapContainer.parents('.mCSB_container').css({
                'width': scrollableWidth ? scaledWidth + 'px' : (wrapperWidth - 50) + 'px',
                'height': scrollableHeight ? scaledHeight + 'px' : (wrapperHeight) + 'px',
            });

            let areaMap = mapContainer.closest('.mCustomScrollbar');
            if(scrollableWidth && scrollableHeight){
                areaMap.mCustomScrollbar('update');
            }else{
                areaMap.mCustomScrollbar('scrollTo', '#' + mapContainer.attr('id'), {
                    scrollInertia: 0,
                    scrollEasing: 'linear',
                    timeout: 0,
                    moveDragger: false
                });
            }
        }

        return zoomChange;
    };

    /**
     * toggles editable input form for system rename (set alias)
     * @param system
     */
    let toggleSystemAliasEditable = system => {
        system.find('.editable').editable('toggle');
    };

    /**
     * mark system as "selected" e.g. for dragging
     * @param map
     * @param system
     * @param select
     */
    let setSystemSelect = (map, system, select) => {
        if(select){
            system.addClass(config.systemSelectedClass);
            map.addToDragSelection(system);
        }else{
            system.removeClass(config.systemSelectedClass);
            map.removeFromDragSelection(system);
        }
    };

    /**
     * mark a system as "active"
     * @param map
     * @param system
     */
    let setSystemActive = (map, system) => {
        // deselect all selected systems on map
        let mapContainer = map.getContainer();
        [...mapContainer.getElementsByClassName(config.systemClass)]
            .forEach(systemEl =>
                systemEl.classList.remove(config.systemActiveClass)
            );

        // set current system active
        system.addClass(config.systemActiveClass);

        // collect all required systemData from map module -> cache
        let systemData = system.getSystemData();
        systemData.mapId = parseInt(system.attr('data-mapid')) || 0;
        Util.setCurrentSystemData(systemData.mapId, systemData);
    };

    /**
     * set system visibility e.g. or filtered systems
     * @param system
     * @param map
     * @param visible
     */
    let setSystemVisible = (system, map, visible) => {
        system = $(system);
        if(!visible){
            // invisible systems should no longer be selected
            setSystemSelect(map, system, false);
        }
        system.toggleClass(config.systemHiddenClass, !visible);
    };

    /**
     * add/remove connection type to connection that was previous registered by registerConnectionTypes()
     * -> this method is a wrapper for addType()/removeType()
     *    with the addition of respecting active Arrow overlay direction
     * @param action
     * @param connection
     * @param types
     * @param params
     * @param doNotRepaint
     */
    let changeConnectionTypes = (action, connection, types = [], params = [], doNotRepaint = false) => {

        if(connection && types.length){
            // check for active Arrow overlay
            let overlayArrow, overlayArrowParams;
            if(
                !types.includes('info_signature') &&
                connection.hasType('info_signature')
            ){
                overlayArrow = connection.getOverlay(MapOverlayUtil.config.connectionOverlayArrowId);
                if(overlayArrow){
                    overlayArrowParams = {
                        direction: overlayArrow.direction,
                        foldback: overlayArrow.foldback,
                    };
                }
            }

            for(let i = 0; i < types.length; i++){
                // change the new type
                connection[action](types[i], typeof params[i] === 'object' ? params[i] : {}, doNotRepaint);
            }

            // change Arrow overlay data back to initial direction
            if(
                overlayArrow &&
                (
                    overlayArrow.direction !== overlayArrowParams.direction ||
                    overlayArrow.foldback !== overlayArrowParams.foldback
                )
            ){
                overlayArrow.updateFrom(overlayArrowParams);
                if(!doNotRepaint){
                    connection.repaint();
                }
            }
        }
    };

    /**
     * add connection type to connection that was previous registered by registerConnectionTypes()
     * @param connection
     * @param type
     * @param params
     * @param doNotRepaint
     */
    let addConnectionType = (connection, type, params, doNotRepaint = false) => {
        addConnectionTypes(connection, [type], typeof params === 'object' ? [params] : [], doNotRepaint);
    };

    let addConnectionTypes = (connection, types = [], params = [], doNotRepaint = false) => {
        if(connection){
            changeConnectionTypes('addType', connection, types.diff(connection.getType()), params, doNotRepaint);
        }
    };

    /**
     * remove connection type to connection that was previous registered by registerConnectionTypes()
     * @param connection
     * @param type
     * @param params
     * @param doNotRepaint
     */
    let removeConnectionType = (connection, type, params, doNotRepaint = false) => {
        removeConnectionTypes(connection, [type], typeof params === 'object' ? [params] : [], doNotRepaint);
    };

    let removeConnectionTypes = (connection, types = [], params = [], doNotRepaint = false) => {
        if(connection){
            changeConnectionTypes('removeType', connection, types.intersect(connection.getType()), params, doNotRepaint);
        }
    };

    let toggleConnectionType = (connection, type, params, doNotRepaint = false) => {
        changeConnectionTypes('toggleType', connection, [type], typeof params === 'object' ? [params] : [], doNotRepaint);
    };

    /**
     * mark a connection as "active"
     * @param map
     * @param connections
     */
    let setConnectionsActive = (map, connections) => {
        map.setSuspendDrawing(true);

        // set all inactive
        for(let connection of getConnectionsByType(map, 'state_active')){
            if(!connections.includes(connection)){
                removeConnectionType(connection, 'state_active');
            }
        }

        for(let connection of connections){
            if(!connection.hasType('state_active')){
                addConnectionType(connection, 'state_active');
            }
        }

        map.setSuspendDrawing(false, true);
    };

    /**
     * set connection visibility e.g. for filtered systems
     * @param connection
     * @param visible
     */
    let setConnectionVisible = (connection, visible) => {
        if(connection.isVisible() !== visible){
            connection.setVisible(visible, true);
            for(let endpoint of connection.endpoints){
                endpoint.setVisible(visible, true);
            }
        }
    };

    /**
     * toggle "selected" status of system
     * @param map
     * @param systems
     */
    let toggleSystemsSelect = (map, systems) => {
        for(let system of systems){
            system = $(system);
            if(system.data('locked') !== true){
                if(system.hasClass(config.systemSelectedClass)){
                    setSystemSelect(map, system, false);
                }else{
                    setSystemSelect(map, system, true);
                }
            }
        }
    };

    /**
     * toggle "selected" status of connections
     * @param map
     * @param connections
     */
    let toggleConnectionActive = (map, connections) => {
        let selectedConnections = [];
        let deselectedConnections = [];

        map.setSuspendDrawing(true);

        for(let connection of connections){
            if(connection.hasType('state_active')){
                removeConnectionType(connection, 'state_active');
                deselectedConnections.push(connection);
            }else{
                addConnectionType(connection, 'state_active');
                selectedConnections.push(connection);
            }
        }

        map.setSuspendDrawing(false, true);

        updateConnectionInfo(map, selectedConnections, deselectedConnections);
    };

    /**
     * show global map info panels
     * @param map
     */
    let showMapInfo = map => {
        // get parent Tab Content and fire update event
        let mapContainer = $(map.getContainer());

        getTabContentElementByMapElement(mapContainer).trigger('pf:renderGlobalModules', {
            mapId: parseInt(mapContainer.data('id')),
            payload: null
        });
    };

    /**
     * show system info panels
     * @param map
     * @param system
     */
    let showSystemInfo = (map, system) => {
        setSystemActive(map, system);

        // get parent Tab Content and fire update event
        let mapContainer = $(map.getContainer());
        let mapId = parseInt(mapContainer.data('id')) || 0;

        getTabContentElementByMapElement(mapContainer).trigger('pf:renderSystemModules', {
            mapId: mapId,
            payload: Util.getCurrentSystemData(mapId)
        });
    };

    /**
     * show connection info panels
     * @param map
     * @param connections
     */
    let showConnectionInfo = (map, connections) => {
        setConnectionsActive(map, connections);

        // get parent Tab Content and fire update event
        let mapContainer = $(map.getContainer());

        getTabContentElementByMapElement(mapContainer).trigger('pf:renderConnectionModules', {
            mapId: parseInt(mapContainer.data('id')),
            payload: connections
        });
    };

    /**
     * update connection info panels
     * @param map
     * @param selectedConnections
     * @param deselectedConnections
     */
    let updateConnectionInfo = (map, selectedConnections, deselectedConnections) => {
        // get parent Tab Content and fire update event
        let mapContainer = $(map.getContainer());

        $(document).trigger('pf:updateConnectionInfoModule', {
            connectionsUpdate: selectedConnections,
            connectionsRemove: deselectedConnections,
            mapId: parseInt(mapContainer.data('id'))
        });
    };

    /**
     * show "find route" dialog -> trigger route panel
     * @param mapContainer
     * @param systemToData
     */
    let showFindRouteDialog = (mapContainer, systemToData) => {
        // get parent Tab Content and fire update event
        getTabContentElementByMapElement(mapContainer).trigger('pf:updateRouteModules', {
            mapId: parseInt(mapContainer.data('id')),
            payload: {
                task: 'showFindRouteDialog',
                systemToData: systemToData
            }
        });
    };

    /**
     * performs a new route search -> trigger route panel
     * @param mapContainer
     * @param systemToData
     */
    let findRoute = (mapContainer, systemToData) => {
        getTabContentElementByMapElement(mapContainer).trigger('pf:updateRouteModules', {
            mapId: parseInt(mapContainer.data('id')),
            payload: {
                task: 'findRoute',
                systemToData: systemToData
            }
        });
    };

    /**
     * search connections by systems
     * @param map
     * @param systems
     * @param scope
     * @returns {Array}
     */
    let searchConnectionsBySystems = (map, systems, scope) => {
        let connections = [];
        let withBackConnection = true;

        $.each(systems, function(i, system){
            // get connections where system is source
            connections = connections.concat( map.getConnections({scope: scope, source: system}) );
            if(withBackConnection === true){
                // get connections where system is target
                connections = connections.concat( map.getConnections({scope: scope, target: system}) );
            }
        });
        return connections;
    };

    /**
     * search connections by scope and/or type
     * -> scope and target can be an array
     * @param map
     * @param scope
     * @param type
     * @param noHidden
     * @returns {Array}
     */
    let searchConnectionsByScopeAndType = (map, scope, type = undefined, noHidden = false) => {
        let connections = [];
        let scopeArray = (scope === undefined) ? ['*'] : ((Array.isArray(scope)) ? scope : [scope]);
        let typeArray = (type === undefined) ? [] : ((Array.isArray(type)) ? type : [type]);

        map.select({scope: scopeArray}).each(function(connection){
            if(noHidden && !connection.isVisible()){
                // exclude invisible connection
                return;
            }

            if(typeArray.length > 0){
                // filter by connection type as well...
                for(let i = 0; i < typeArray.length; i++){
                    if( connection.hasType(typeArray[i]) ){
                        connections.push(connection);
                        break; // don´t add same connection multiple times
                    }
                }
            }else{
                // connection type is ignored
                connections.push(connection);
            }
        });

        return connections;
    };

    /**
     * get Connection Info by option
     * @param {string} connectionTyp
     * @param {string} option
     * @returns {string}
     */
    let getConnectionInfo = (connectionTyp, option) => {
        let connectionInfo = '';
        if(Init.connectionTypes.hasOwnProperty(connectionTyp)){
            connectionInfo = Init.connectionTypes[connectionTyp][option];
        }
        return connectionInfo;
    };

    /**
     * get CSS classes for connection types
     * @param types
     * @returns {string[]}
     */
    let getConnectionFakeClassesByTypes = types => {
        let connectionClasses = ['pf-fake-connection'];
        for(let i = 0; i < types.length; i++){
            connectionClasses.push(getConnectionInfo(types[i], 'cssClass'));
        }
        return connectionClasses;
    };

    /**
     * get all direct connections between two given systems
     * @param map
     * @param systemA
     * @param systemB
     * @returns {*[]}
     */
    let checkForConnection = (map, systemA, systemB) => {
        let connections = [];
        connections = connections.concat( map.getConnections({scope: '*', source: systemA, target: systemB}) );
        // get connections where system is target
        connections = connections.concat( map.getConnections({scope: '*', source: systemB, target: systemA}) );
        return connections;
    };

    /**
     * get the default connection type for a scope
     * e.g. for new type after scope change
     * @param {string} scope
     * @returns {string}
     */
    let getDefaultConnectionTypeByScope = scope => {
        let type = '';
        switch(scope){
            case 'wh':
                type = 'wh_fresh'; break;
            case 'jumpbridge':
                type = 'jumpbridge'; break;
            case'stargate':
                type = 'stargate'; break;
            case'abyssal':
                type = 'abyssal'; break;
            default:
                console.error('Connection scope "' + scope + '" unknown!');
        }

        return type;
    };

    /**
     * get all available connection types for "mass status"
     * @returns {string[]}
     */
    let allConnectionMassStatusTypes = () => {
        return ['wh_fresh', 'wh_reduced', 'wh_critical'];
    };

    /**
     * get all available connection types for "jump mass size"
     * @returns {string[]}
     */
    let allConnectionJumpMassTypes = () => {
        return ['wh_jump_mass_s', 'wh_jump_mass_m', 'wh_jump_mass_l', 'wh_jump_mass_xl'];
    };

    /**
     * set/change/remove connection mass status of connection
     * -> statusType == undefined will remove (all) existing mass status types
     * @param connection
     * @param statusType
     */
    let setConnectionMassStatusType = (connection, statusType) => {
        setUniqueConnectionType(connection, statusType, allConnectionMassStatusTypes());

    };

    /**
     * set/change/remove connection jump mass of a connection
     * -> massType == undefined will remove (all) existing jump mass types
     * @param connection
     * @param massType
     */
    let setConnectionJumpMassType = (connection, massType) => {
        setUniqueConnectionType(connection, massType, allConnectionJumpMassTypes());
    };

    /**
     * set/change/remove connection type
     * -> type == undefined will remove (all) existing provided types
     * @param connection
     * @param type
     * @param types
     */
    let setUniqueConnectionType = (connection, type, types) => {
        type = types.includes(type) ? [type] : [];

        connection._jsPlumb.instance.setSuspendDrawing(true);

        // ... redraw should be suspended (no repaint until function ends)
        let doNotRepaint = connection._jsPlumb.instance.isSuspendDrawing();
        removeConnectionTypes(connection, types.diff(type), [], doNotRepaint);
        addConnectionTypes(connection, type, [], doNotRepaint);

        connection._jsPlumb.instance.setSuspendDrawing(false, true);
    };

    /**
     * get some scope info for a given info string
     * @param {string} info
     * @param {string} option
     * @returns {string}
     */
    let getScopeInfoForConnection = (info, option) => {
        let scopeInfo = '';
        if(Init.connectionScopes.hasOwnProperty(info)){
            switch(option){
                case 'connectorDefinition':
                    // json data in DB
                    let temp = '{ "data": ' + Init.connectionScopes[info][option] + '}';
                    scopeInfo = $.parseJSON( temp).data;
                    break;
                default:
                    scopeInfo = Init.connectionScopes[info][option];
                    break;
            }
        }

        return scopeInfo;
    };

    /**
     * show map animations when a new map gets visual
     * @param mapElement
     * @param show
     * @returns {Promise<any>}
     */
    let visualizeMap = (mapElement, show) => {

        let visualizeMapExecutor = (resolve, reject) => {
            // start map update counter -> prevent map updates during animations
            MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter();

            let systemElements = mapElement.find('.' + config.systemClass);
            let endpointElements =  mapElement.find('.jtk-endpoint:visible');
            let connectorElements = mapElement.find('.jtk-connector:visible');
            let overlayElements = mapElement.find('.jtk-overlay:visible, .tooltip');

            let hideElements = (elements) => {
                if(elements.length > 0){
                    // disable transition for next opacity change
                    elements.addClass('pf-notransition');
                    // hide elements
                    elements.css('opacity', 0);
                    // Trigger a reflow, flushing the CSS changes
                    // -> http://stackoverflow.com/questions/11131875/what-is-the-cleanest-way-to-disable-css-transition-effects-temporarily
                    elements[0].offsetHeight; // jshint ignore:line
                    elements.removeClass('pf-notransition');
                }

                return elements;
            };

            let mapElements = systemElements.add(endpointElements).add(connectorElements);

            // show nice animation
            if(show === 'show'){
                hideElements(systemElements);
                hideElements(endpointElements);
                hideElements(connectorElements);
                hideElements(overlayElements);

                overlayElements.velocity('transition.fadeIn', {
                    duration: 60,
                    display: 'auto'
                });

                if(mapElements.length){
                    mapElements.velocity({
                        translateY: [ 0, -20],
                        opacity: [ 1, 0 ]
                    }, {
                        duration: 150,
                        easing: 'easeOut',
                        complete: function(){
                            resolve({
                                action: 'visualizeMap',
                                data: false
                            });
                        }
                    });
                }else{
                    // "complete" callback is not fired if no elements were animated
                    resolve({
                        action: 'visualizeMap',
                        data: false
                    });
                }

            }else if(show === 'hide'){

                overlayElements.velocity('transition.fadeOut', {
                    duration: 60,
                    display: 'auto'
                });

                if(mapElements.length){
                    mapElements.velocity({
                        translateY: [ -20, 0 ],
                        opacity: [ 0, 1 ]
                    }, {
                        duration: 150,
                        easing: 'easeOut',
                        complete: function(){
                            resolve({
                                action: 'visualizeMap',
                                data: false
                            });
                        }
                    });
                }else{
                    // "complete" callback is not fired if no elements were animated
                    resolve({
                        action: 'visualizeMap',
                        data: false
                    });
                }

            }
        };

        return new Promise(visualizeMapExecutor);
    };

    /**
     * Set default map options (from right menu)
     * This function is called only ONCE per map after create!
     * -> HINT: This function triggers events! Promise is resolved before trigger callback finishes
     * @param mapElement
     * @param mapConfig
     * @returns {Promise<any>}
     */
    let setMapDefaultOptions = (mapElement, mapConfig) => {

        let setMapDefaultOptionsExecutor = (resolve, reject) => {
            // update main menu options based on the active map -----------------------------------------------
            $(document).trigger('pf:updateMenuOptions', {
                menuGroup: 'mapOptions',
                payload: mapConfig
            });

            // init grid snap ---------------------------------------------------------------------------------
            Util.triggerMenuAction(mapElement, 'MapOption', {
                option: 'mapSnapToGrid',
                toggle: false
            });

            // init magnetizer --------------------------------------------------------------------------------
            Util.triggerMenuAction(mapElement, 'MapOption', {
                option: 'mapMagnetizer',
                toggle: false
            });

            // init compact system layout ---------------------------------------------------------------------
            Util.triggerMenuAction(mapElement, 'MapOption', {
                option: 'systemRegion',
                toggle: false
            });

            // init compact system layout ---------------------------------------------------------------------
            Util.triggerMenuAction(mapElement, 'MapOption', {
                option: 'systemCompact',
                toggle: false
            });

            // init endpoint overlay --------------------------------------------------------------------------
            Util.triggerMenuAction(mapElement, 'MapOption', {
                option: 'connectionSignatureOverlays',
                toggle: false,
                skipOnEnable: true,     // skip callback -> Otherwise it would run 2 times on map create
                skipOnDisable: true     // skip callback -> Otherwise it would run 2 times on map create
            });

            resolve({
                action: 'setMapDefaultOptions',
                data: false
            });
        };

        return new Promise(setMapDefaultOptionsExecutor);
    };

    /**
     * get system coordinates from systemElement
     * @param system
     * @returns {{x: number, y: number}}
     */
    let getSystemPosition = system => {
        let x = system.css('left');
        let y = system.css('top');

        return {
          x: parseInt(x.substring(0, x.length - 2)),
          y: parseInt(y.substring(0, y.length - 2))
        };
    };

    /**
     * scroll map to default (stored) x/y coordinates
     * @param map
     * @returns {Promise<any>}
     */
    let scrollToDefaultPosition = map => {

        let scrollToDefaultPositionExecutor = resolve => {
            let payload = {
                action: 'scrollToDefaultPosition',
                data: false
            };

            // no map scroll on zoomed maps -> scrollbar offset on zoomed maps does not work properly
            // -> implementation would be difficult...
            if(map.getZoom() === 1){
                let mapElement = $(map.getContainer());
                Util.getLocalStore('map').getItem(mapElement.data('id')).then(data => {
                    if(data && data.scrollOffset){
                        let areaMap = mapElement.closest('.' + Util.getMapTabContentAreaClass('map'));
                        Scrollbar.scrollToPosition(areaMap, [data.scrollOffset.y, data.scrollOffset.x]);
                    }

                    resolve(payload);
                });
            }else{
                resolve(payload);
            }
        };

        return new Promise(scrollToDefaultPositionExecutor);
    };

    /**
     * zoom map to default (stored) scale()
     * @param map
     * @returns {Promise<any>}
     */
    let zoomToDefaultScale = map => {

        let zoomToDefaultScaleExecutor = resolve => {
            let mapElement = $(map.getContainer());
            Util.getLocalStore('map').getItem(mapElement.data('id')).then(data => {
                if(data && data.mapZoom){
                    setZoom(map, data.mapZoom);
                }

                resolve({
                    action: 'zoomToDefaultScale',
                    data: false
                });
            });
        };

        return new Promise(zoomToDefaultScaleExecutor);
    };

    /**
     * set or change rallyPoint for systems
     * @param rallyUpdated
     * @param options
     * @returns {*}
     */
    $.fn.setSystemRally = function(rallyUpdated, options){
        rallyUpdated = rallyUpdated || 0;
        let rallyPoke = false;

        let defaultOptions = {
            poke: false,
            hideNotification: false,    // do not show notification
            hideCounter: false          // do not start map update counter
        };
        options = $.extend({}, defaultOptions, options);

        return this.each(function(){
            let system = $(this);
            let rally = system.data('rallyUpdated') || 0;

            if(rallyUpdated !== rally){
                // rally status changed
                if( !options.hideCounter ){
                    MapOverlayUtil.getMapOverlay(system, 'timer').startMapUpdateCounter();
                }

                let rallyClass = getInfoForSystem('rally', 'class');

                if(rallyUpdated > 0){
                    // new rally point set OR update system with rally information
                    system.addClass(rallyClass);

                    // rallyUpdated > 0 is required for poke!
                    rallyPoke = options.poke;

                    let notificationOptions = {
                        title: 'Rally Point',
                        text: 'System: ' +  system.data('name')
                    };

                    if(rallyUpdated === 1){
                        // rally point not saved on DB
                        notificationOptions.type = 'success';
                        Util.showNotify(notificationOptions);
                    }else if(options.poke){
                        // rally saved AND poke option active

                        // check if desktop notification was already send
                        let mapId = system.data('mapid');
                        let systemId = system.data('id');
                        Util.getLocalStore('map').getItem(mapId).then(function(data){
                            // This code runs once the value has been loaded
                            // from the offline store.
                            let rallyPokeData = {};

                            if(
                                data &&
                                data.rallyPoke
                            ){
                                // poke data exists
                                rallyPokeData = data.rallyPoke;
                            }

                            if(
                                !rallyPokeData.hasOwnProperty(this.systemId) || // rally poke was not already send to client
                                rallyPokeData[this.systemId] !== rallyUpdated // already send to that system but in the past
                            ){
                                rallyPokeData[this.systemId] = rallyUpdated;
                                Util.getLocalStore('map').setItem(`${this.mapId}.rallyPoke`, rallyPokeData);

                                notificationOptions.type = 'info';
                                Util.showNotify(notificationOptions, {
                                    desktop: true,
                                    stack: 'barBottom',
                                    click: e => {window.location.href = getMapDeeplinkUrl(mapId, systemId);}
                                });
                            }
                        }.bind({
                            mapId: mapId,
                            systemId: systemId,
                            rallyUpdated: rallyUpdated
                        }));
                    }

                    // update active "route" module -> add rally point row --------------------------------------------
                    let mapContainer = system.parents('.' + Util.config.mapClass);
                    findRoute(mapContainer, {
                        systemId: system.data('systemId'),
                        name: system.data('name'),
                        rally: 1
                    });
                }else{
                    // rally point removed
                    system.removeClass(rallyClass);

                    if( !options.hideNotification ){
                        Util.showNotify({title: 'Rally point removed', type: 'success'});
                    }
                }
            }

            system.data('rallyUpdated', rallyUpdated);
            system.data('rallyPoke', rallyPoke);
        });
    };

    /**
     * set map "shortcut" events
     */
    $.fn.setMapShortcuts = function(){
        return this.each((i, areaMap) => {
            areaMap = $(areaMap);
            let mapElement = areaMap.find('.' + Util.config.mapClass);

            // dynamic require Map module -> otherwise there is a require(), loop
            let Map = require('app/map/map');
            let System = require('app/map/system');
            let map = Map.getMapInstance(mapElement.data('id'));

            areaMap.watchKey('mapSystemAdd', areaMap => {
                System.showNewSystemDialog(map, {position: {x: 0, y: 0}}, Map.saveSystemCallback);
            },{focus: true});

            areaMap.watchKey('mapSystemsSelect', areaMap => {
                mapElement.selectAllSystems();
            },{focus: true});

            areaMap.watchKey('mapSystemsDelete', areaMap => {
                let selectedSystems = mapElement.getSelectedSystems();
                $.fn.showDeleteSystemDialog(map, selectedSystems);
            },{focus: true});
        });
    };

    /**
     * add system pilot tooltip
     * @param systemUserData
     * @param options
     * @returns {*}
     */
    $.fn.addSystemPilotTooltip = function(systemUserData, options){
        let content = Util.getSystemPilotsTable(systemUserData);

        let defaultOptions = {
            placement: 'top',
            html: true,
            trigger: 'hover',
            container: 'body',
            title: 'Pilots',
            content: content,
            delay: {
                show: 150,
                hide: 0
            },
        };

        options = $.extend({}, defaultOptions, options);

        return this.each(function(){
            $(this).popover(options);
        });
    };

    /**
     * add station services tooltip
     * @param services
     * @param options
     * @returns {*}
     */
    $.fn.addStationServiceTooltip = function(services, options){
        let getServiceIcon = service => {
            switch(service){
                case 'bounty-missions':         return false;
                case 'assasination-missions':   return false;
                case 'courier-missions':        return false;
                case 'interbus':                return false;
                case 'reprocessing-plant':      return 'reprocess';
                case 'refinery':                return false;
                case 'market':                  return 'market';
                case 'black-market':            return false;
                case 'stock-exchange':          return false;
                case 'cloning':                 return 'clonebay';
                case 'surgery':                 return false;
                case 'dna-therapy':             return false;
                case 'repair-facilities':       return 'repairshop';
                case 'factory':                 return 'industry';
                case 'labratory':               return 'research';
                case 'gambling':                return false;
                case 'fitting':                 return 'fitting';
                case 'paintshop':               return 'skins';
                case 'news':                    return false;
                case 'storage':                 return false;
                case 'insurance':               return 'insurance';
                case 'docking':                 return 'docking';
                case 'office-rental':           return false;
                case 'jump-clone-facility':     return 'jumpclones';
                case 'loyalty-point-store':     return 'lpstore';
                case 'navy-offices':            return 'factionalwarfare';
                case 'security-offices':        return 'concord';
                default:                        return false;
            }
        };

        let getStationServicesTable = services => {
            let content = '';
            for(let i = 0; i < services.length; i++){
                let icon = getServiceIcon(services[i]);
                if(icon){
                    content += `<img class="${Util.config.popoverListIconClass}" src="${Util.imgRoot()}icons/client/ui/window/${icon}.png" alt="${services[i]}">`;
                }
            }
            return content;
        };

        let content = getStationServicesTable(services);

        let title = '<i class="fas fa-tools fa-fw"></i>&nbsp;Services';

        let defaultOptions = {
            placement: 'top',
            html: true,
            trigger: 'hover',
            container: 'body',
            title: title,
            content: '',
            delay: {
                show: 150,
                hide: 0
            },
        };

        options = $.extend({}, defaultOptions, options);

        return this.each(function(){
            let element = $(this);
            element.popover(options);

            // set new popover content
            let popover = element.data('bs.popover');
            popover.options.content = content;
            popover.tip().addClass(Util.config.popoverClass);

            if(options.show){
                element.popover('show');
            }
        });
    };

    /**
     * add system effect tooltip
     * @param security
     * @param effect
     * @param options
     * @returns {*}
     */
    $.fn.addSystemEffectTooltip = function(security, effect, options){
        let effectClass = getEffectInfoForSystem(effect, 'class');
        let systemEffectData = Util.getSystemEffectData(security, effect);
        let areaId = Util.getAreaIdBySecurity(security);

        let title = '<i class="fas fa-square fa-fw ' + effectClass + '"></i>&nbsp;' +
            getEffectInfoForSystem(effect, 'name') + '&nbsp;&nbsp;' +
            '<kbd>' + Util.getSystemEffectMultiplierByAreaId(parseInt(areaId)) + 'x' + '</kbd>' +
            '<span class="pull-right ' + Util.getSecurityClassForSystem(security) + '">' + security + '</span>';

        let content = Util.getSystemEffectTable(systemEffectData);

        let defaultOptions = {
            placement: 'top',
            html: true,
            trigger: 'hover',
            container: 'body',
            title: title,
            content: content,
            delay: {
                show: 150,
                hide: 0
            },
        };

        options = $.extend({}, defaultOptions, options);

        return this.each(function(){
            $(this).popover(options);
        });
    };

    /**
     * add system planets tooltip
     * @param planets
     * @param options
     * @returns {*}
     */
    $.fn.addSystemPlanetsTooltip = function(planets, options){
        let content = Util.getSystemPlanetsTable(planets);

        let defaultOptions = {
            placement: 'top',
            html: true,
            trigger: 'hover',
            container: 'body',
            title: 'Planets',
            content: content,
            delay: {
                show: 150,
                hide: 0
            },
        };

        options = $.extend({}, defaultOptions, options);

        return this.each(function(){
            if(!planets) return '<table></table>'
            if(planets.length){
                // Abyss systems don´t have planets -> no tooltip
                let element = $(this);
                element.popover(options);

                if(options.show){
                    element.popover('show');
                }
            }
        });
    };

    /**
     * add a wormhole tooltip with wh specific data to elements
     * @param tooltipData
     * @param options
     * @returns {*}
     */
    $.fn.addWormholeInfoTooltip = function(tooltipData, options){
        return this.each(function(){
            let element = $(this);

            requirejs(['text!templates/tooltip/wormhole_info.html', 'mustache'], (template, Mustache) => {

                // format tooltip data
                let data = {};
                if(tooltipData.massTotal){
                    data.massTotal = Util.formatMassValue(tooltipData.massTotal);
                }else{
                    data.massTotal = '<span class="txt-color txt-color-grayLight">unknown</span>';
                }
                if(tooltipData.massIndividual){
                    data.massIndividual = Util.formatMassValue(tooltipData.massIndividual);
                }else{
                    data.massIndividual = '<span class="txt-color txt-color-grayLight">unknown</span>';
                }
                if(tooltipData.massRegeneration){
                    data.massRegeneration = Util.formatMassValue(tooltipData.massRegeneration);
                }
                if(tooltipData.maxStableTime){
                    data.maxStableTime = tooltipData.maxStableTime + ' h';
                }
                if(tooltipData.scanWormholeStrength){
                    data.scanWormholeStrength = parseFloat(tooltipData.scanWormholeStrength).toLocaleString() + '&nbsp;&#37;';
                }else{
                    data.scanWormholeStrength = '<span class="txt-color txt-color-grayLight">unknown</span>';
                }

                let title = tooltipData.name;

                if(tooltipData.size){
                    title += '&nbsp;&nbsp;<kbd>' + tooltipData.size.label + '</kbd>';
                }

                if(tooltipData.security){
                    // K162 has no security
                    title += '<span class="pull-right ' + tooltipData.class +'">' + tooltipData.security + '</span>';
                }

                let content = Mustache.render(template, data);

                let defaultOptions = {
                    placement: 'top',
                    html: true,
                    trigger: 'hover',
                    container: 'body',
                    title: title,
                    content: '',
                    delay: {
                        show: 150,
                        hide: 0
                    }
                };

                options = $.extend({}, defaultOptions, options);

                element.popover(options);

                // set new popover content
                let popover = element.data('bs.popover');
                popover.options.title = title;
                popover.options.content = content;

                if(options.smaller){
                    element.setPopoverSmall();
                }

                if(options.show){
                    element.popover('show');
                }
            });
        });
    };

    /**
     *
     * @param container any parent element that holds the event
     * @param selector element that bubbles up hover
     * @param options tooltip options
     */
    let initWormholeInfoTooltip = (container, selector, options = {}) => {
        let defaultOptions = {
            trigger: 'manual',
            placement: 'top',
            smaller: false,
            show: true
        };

        options = Object.assign({}, defaultOptions, options);

        container.hoverIntent({
            over: function(e){
                let staticWormholeElement = $(this);
                let wormholeName = staticWormholeElement.attr('data-name');
                let wormholeData = Util.getObjVal(Init, 'wormholes.' + wormholeName);
                if(wormholeData){
                    staticWormholeElement.addWormholeInfoTooltip(wormholeData, options);
                }
            },
            out: function(e){
                $(this).destroyPopover();
            },
            selector: selector
        });
    };

    /**
     * get systemId string (selector
     * @param mapId
     * @param systemId
     * @returns {string}
     */
    let getSystemId = (mapId, systemId) => {
        return config.systemIdPrefix + mapId + '-' + systemId;
    };

    /**
     * check whether the current User has access for a given right
     * based on a given mapConfig
     * @param right
     * @param mapConfig
     * @returns {boolean}
     */
    let checkRight = (right, mapConfig) => {
        let hasAccess = false;
        let mapType = Util.getObjVal(mapConfig, 'type.name');
        let accessObjectId = Util.getCurrentUserInfo(mapType + 'Id');

        // check whether character has map access
        let mapAccess = Util.getObjVal(mapConfig, 'access.' + (mapType === 'private' ? 'character' : mapType)) || [];
        let hasMapAccess = mapAccess.some(accessObj => accessObj.id === accessObjectId);
        if(hasMapAccess){
            // ... this should ALWAYS be be true!
            switch(mapType){
                case 'private':
                    hasAccess = true;
                    break;
                case 'corporation':
                    hasAccess = Util.hasRight(right, mapType);
                    break;
                case 'alliance':
                    hasAccess = true;
                    break;
            }
        }
        return hasAccess;
    };

    /**
     *
     * @param options
     * @param maxResults
     * @param findChain
     * @returns {Array}
     */
    let findNonOverlappingDimensions = (options = {}, maxResults = 1, findChain = false) => {
        let defaultOptions = {
            center: [0, 30],
            loops: 4,
            debug: false
        };

        options = Object.assign({}, defaultOptions, options);
        let positionFinder = new Layout.Position(Object.assign({}, defaultOptions, options));

        return positionFinder.findNonOverlappingDimensions(maxResults, findChain);
    };

    /**
     * calculate the x/y coordinates for a new system - relative to a source system
     * @param sourceSystem
     * @returns {Array}
     */
    let newSystemPositionBySystem = sourceSystem => {
        let mapContainer = sourceSystem.parent();
        let grid = [config.mapSnapToGridDimension, config.mapSnapToGridDimension];

        let options = {
            container: mapContainer[0],
            center: sourceSystem[0],
            grid: mapContainer.hasClass(config.mapGridClass) ? grid : false
        };

        return findNonOverlappingDimensions(options);
    };

    /**
     * calculate the x/y coordinates for a new system - relative to x/y position
     * @param mapContainer
     * @param options
     * @param maxResults
     * @param findChain
     * @returns {Array}
     */
    let newSystemPositionByCoordinates = (mapContainer, options = {}, maxResults = 1, findChain = false) => {
        let grid = [config.mapSnapToGridDimension, config.mapSnapToGridDimension];

        let defaultOptions = {
            container: mapContainer[0],
            center: [0, 0],
            grid: mapContainer.hasClass(config.mapGridClass) ? grid : false,
            loops: 10,
            defaultGapX: 10,
            defaultGapY: 10,
            //debugOk: true,
            //debug: true
        };

        options = Object.assign({}, defaultOptions, options);

        return findNonOverlappingDimensions(options, maxResults, findChain);
    };

    /**
     * calculate the x/y coordinates for a new system - relative to current map scroll offset
     * @param mapContainer
     * @param maxResults
     * @returns {[{x: number, y: number}]}
     */
    let newSystemPositionsByMapOffset = (mapContainer, maxResults = 1) => {
        let scrollPosition = {
            x: Math.abs(parseInt(mapContainer.attr('data-scroll-left')) || 0),
            y: Math.abs(parseInt(mapContainer.attr('data-scroll-top')) || 0)
        };

        // space new positions from map top (e.g. used for tooltips)
        scrollPosition.y = Math.max(scrollPosition.y, 30);

        // default position -> current map section top/left
        let positions = [scrollPosition];

        // check default position for overlapping
        let dimensions = newSystemPositionByCoordinates(mapContainer, {
            center: [scrollPosition.x, scrollPosition.y],
            minX: scrollPosition.x,
            minY: scrollPosition.y
        }, maxResults, true);

        if(dimensions.length){
            positions = dimensions.map(dim => ({
                x: parseInt(dim.left) || 0,
                y: parseInt(dim.top) || 0
            }));
        }
        return positions;
    };

    /**
     *
     * @param mapContainer
     */
    let newSystemPositionsByMap = mapContainer => {
        let positions = {};

        if(mapContainer){
            let mapId = mapContainer.data('id');

            positions.defaults = newSystemPositionsByMapOffset(mapContainer, 2);

            // -> calc possible coordinates for new system that should be used based on current user location ---------
            let currentLocationData = Util.getCurrentLocationData();
            if(currentLocationData.id){
                // ... we need to the PF systemId for 'SelectSystem' trigger
                let systemData = getSystemData(mapId, currentLocationData.id, 'systemId');
                if(systemData){
                    let currentSystem = $('#' + getSystemId(mapId, systemData.id));
                    if(currentSystem.length){
                        let dimensions = newSystemPositionBySystem(currentSystem);
                        if(dimensions.length){
                            //... empty map space found
                            positions.location = {
                                systemId: currentLocationData.id,
                                position: {
                                    x: dimensions[0].left,
                                    y: dimensions[0].top
                                }
                            };
                        }
                    }
                }
            }
        }

        return Object.keys(positions).length ? positions : null;
    };

    /**
     * get a unique map url for deeplinking
     * @param mapId
     * @param systemId
     * @returns {string}
     */
    let getMapDeeplinkUrl = (mapId, systemId) => {
        let url = location.protocol + '//' + location.host + '/map';
        url += mapId ? '/' + encodeURIComponent(window.btoa(mapId)) : '';
        url += systemId ? '_' + encodeURIComponent(window.btoa(systemId)) : '';
        return url;
    };

    return {
        config: config,
        setMapInstance: setMapInstance,
        getMapInstance: getMapInstance,
        existsMapInstance: existsMapInstance,
        clearMapInstance: clearMapInstance,
        getMapTypes: getMapTypes,
        getMapScopes: getMapScopes,
        getScopeInfoForMap: getScopeInfoForMap,
        getMapIcons: getMapIcons,
        getInfoForMap: getInfoForMap,
        getInfoForSystem: getInfoForSystem,
        getSystemDataFromMapData: getSystemDataFromMapData,
        getSystemData: getSystemData,
        getConnectionDataFromMapData: getConnectionDataFromMapData,
        getConnectionData: getConnectionData,
        getSystemTypeInfo: getSystemTypeInfo,
        getEffectInfoForSystem: getEffectInfoForSystem,
        markAsChanged: markAsChanged,
        hasChanged: hasChanged,
        toggleSystemsSelect: toggleSystemsSelect,
        addConnectionTypes: addConnectionTypes,
        removeConnectionTypes: removeConnectionTypes,
        toggleConnectionType: toggleConnectionType,
        toggleConnectionActive: toggleConnectionActive,
        setSystemActive: setSystemActive,
        showMapInfo: showMapInfo,
        showSystemInfo: showSystemInfo,
        showConnectionInfo: showConnectionInfo,
        showFindRouteDialog: showFindRouteDialog,
        filterDefaultTypes: filterDefaultTypes,
        getEndpointLabel: getEndpointLabel,
        getConnectionsByType: getConnectionsByType,
        getEndpointsDataByConnection: getEndpointsDataByConnection,
        getDataByConnection: getDataByConnection,
        getDataByConnections: getDataByConnections,
        searchConnectionsBySystems: searchConnectionsBySystems,
        searchConnectionsByScopeAndType: searchConnectionsByScopeAndType,
        getConnectionInfo: getConnectionInfo,
        getConnectionFakeClassesByTypes: getConnectionFakeClassesByTypes,
        checkForConnection: checkForConnection,
        getDefaultConnectionTypeByScope: getDefaultConnectionTypeByScope,
        allConnectionMassStatusTypes: allConnectionMassStatusTypes,
        allConnectionJumpMassTypes: allConnectionJumpMassTypes,
        setConnectionMassStatusType: setConnectionMassStatusType,
        setConnectionJumpMassType: setConnectionJumpMassType,
        getScopeInfoForConnection: getScopeInfoForConnection,
        deleteConnections: deleteConnections,
        getConnectionDataFromSignatures: getConnectionDataFromSignatures,
        getEndpointOverlaySignatureLocation: getEndpointOverlaySignatureLocation,
        formatEndpointOverlaySignatureLabel: formatEndpointOverlaySignatureLabel,
        getTabContentElementByMapElement: getTabContentElementByMapElement,
        hasActiveConnection: hasActiveConnection,
        filterMapByScopes: filterMapByScopes,
        changeZoom: changeZoom,
        setZoom: setZoom,
        toggleSystemAliasEditable: toggleSystemAliasEditable,
        visualizeMap: visualizeMap,
        setMapDefaultOptions: setMapDefaultOptions,
        getSystemPosition: getSystemPosition,
        scrollToDefaultPosition: scrollToDefaultPosition,
        zoomToDefaultScale: zoomToDefaultScale,
        initWormholeInfoTooltip: initWormholeInfoTooltip,
        getSystemId: getSystemId,
        checkRight: checkRight,
        newSystemPositionBySystem: newSystemPositionBySystem,
        newSystemPositionByCoordinates: newSystemPositionByCoordinates,
        newSystemPositionsByMapOffset: newSystemPositionsByMapOffset,
        newSystemPositionsByMap: newSystemPositionsByMap,
        getMapDeeplinkUrl: getMapDeeplinkUrl
    };
});