// Класс для доступа к данным маршрутов
class WaypointsData {
    constructor() {
        // Данные для маркеров на карте
        this.markersData = [];
        // Регионы
        this.regions = [];
        // Точки
        this.points = [];
    }

    // Заполнение данных
    loadData() {
        let point_counter = 0;
        for (const r_idx in data['regions']) {
            const region = data['regions'][r_idx]
            this.regions.push({
                id: r_idx,
                name: region.name,
                icon: 'airplane'
            });
            for (const p_idx in region.points) {
                const point = region.points[p_idx];
                const loc = point.gps.split(',');
                const lat = parseFloat(loc[0].trim())
                const lon = parseFloat(loc[1].trim())
                this.markersData.push({
                    id: point_counter,
                    regionId: r_idx,
                    location: [lat, lon],
                    tooltip: {
                        text: point.name,
                    }
                });
                this.points.push({
                    id: point_counter,
                    location: [lat, lon],
                    text: point.name,
                    description: point.description,
                    regionId: r_idx,
                });
                point_counter++;
            }
        }
    }

    // Получить маркеры для карты
    getMarkers() {
        return this.markersData;
    }

    // Получить список регионов
    getRegions() {
        return this.regions;
    }

    // Получить точки которые относятся к региону
    getRegionPoints(r_idx) {
        return this.points.filter(p => p.regionId == r_idx);
    }

    // Получить точки по переданным индексам точек
    getSelectedPoints(indexes) {
        let result = [];
        this.points.forEach((point) => {
            if (indexes.includes(point.id)) {
                result.push(point);
            }
        });

        // Сортировка результата в порядке переданных индексов
        let ordered = [];
        indexes.forEach((ind) => {
            const index = result.findIndex(p => p.id == ind);
            ordered.push(result[index]);
            result.slice(index, 1);
        });
        return ordered;
    }
}

// Точки для карты
const waypoints = new WaypointsData();
const markerUrl = './img/map-marker.png';
// Максимальное число путей для перебора
const max_ways_count = 300000;
let regionPointsWidget;
let selectedPointsWidget;

// Инициализация контрола карты
function initMap() {
    const mapWidget = $('#map').dxMap({
        provider: 'bing',
        apiKey: {
            bing: 'AnpoY0kAA4Kk5A045nQxyVbrlkNTgOuMVBitLxN_iLnZdtONf21HxUTzXwNIebES',
        },
        zoom: 11,
        height: 440,
        width: 760,
        controls: true,
        markerIconSrc: markerUrl,
        markers: waypoints.getMarkers(),
        type: 'roadmap'
    }).dxMap('instance');
}

function reInitMap(regionId) {
    const mapWidget = $('#map').dxMap({
        provider: 'bing',
        apiKey: {
            bing: 'AnpoY0kAA4Kk5A045nQxyVbrlkNTgOuMVBitLxN_iLnZdtONf21HxUTzXwNIebES',
        },
        zoom: 11,
        height: 440,
        width: 760,
        controls: true,
        markerIconSrc: markerUrl,
        markers: waypoints.getMarkers().filter(p => p.regionId == regionId),
        type: 'roadmap'
    }).dxMap('instance');
}

function reInitMapByPoints(indexes) {
    const mapWidget = $('#map').dxMap({
        provider: 'bing',
        apiKey: {
            bing: 'AnpoY0kAA4Kk5A045nQxyVbrlkNTgOuMVBitLxN_iLnZdtONf21HxUTzXwNIebES',
        },
        zoom: 11,
        height: 440,
        width: 760,
        controls: true,
        markerIconSrc: markerUrl,
        markers: waypoints.getMarkers().filter(p => indexes.includes(p.id)),
        type: 'roadmap'
    }).dxMap('instance');
}

// Пересоздание списка точек указанного региона, для выбора мест к посещению
function rebuildRegionPointsList(regionId) {
    if (regionPointsWidget) {
        regionPointsWidget.dispose();
    }
    if (selectedPointsWidget) {
        selectedPointsWidget.dispose();
    }
    regionPointsWidget = $('#regionPointsList').dxList({
        dataSource: new DevExpress.data.DataSource({
            store: new DevExpress.data.ArrayStore({
                key: 'id',
                data: waypoints.getRegionPoints(regionId),
            }),
        }),
        height: 400,
        showScrollbar: 'always',
        showSelectionControls: true,
        selectionMode: 'all',
        selectByClick: false,
        paginate: false,
        pageLoadMode: "scrollBottom",
        onSelectionChanged() {
            rebuildSelectedPointsList(regionPointsWidget.option('selectedItemKeys'));
        },
        itemTemplate(data) {
            const result = $('<div>').addClass('region-point');
            $('<h3>').text(data.text).appendTo(result);
            $('<i>').text(data.description).appendTo(result);
            return result;
        }
    }).dxList('instance');
}

// Инициализация выпадающего списка с регионами
function initRegionDropDown() {
    $('#one-section').dxDropDownButton({
        text: 'Выбор региона',
        icon: 'globe',
        dropDownOptions: {
            width: 230,
        },
        displayExpr: 'name',
        keyExpr: 'id',
        onItemClick(e) {
            rebuildRegionPointsList(e.itemData.id);
            reInitMap(e.itemData.id);
            DevExpress.ui.notify(`${e.itemData.name}`, 'success', 600);
        },
        items: waypoints.getRegions(),
    });
}

// Перестроение списка выбранных точек
function rebuildSelectedPointsList(selectedPointsIndexes) {
    const selected = waypoints.getSelectedPoints(selectedPointsIndexes);
    if (selectedPointsWidget) {
        selectedPointsWidget.dispose();
    }
    selectedPointsWidget = $("#selectedPointsList").dxList({
        dataSource: selected,
        keyExpr: 'id',
        height: 400,
        width: '100%',
        showScrollbar: 'always',
        paginate: false,
        pageLoadMode: "scrollBottom",
        itemDragging: {
            allowReordering: true,
            data: selected,
            group: 'selected',
            onDragStart(e) {
                e.itemData = e.fromData[e.fromIndex];
            },
            onAdd(e) {
                e.toData.splice(e.toIndex, 0, e.itemData);
                e.component.reload();
            },
            onRemove(e) {
                e.fromData.splice(e.fromIndex, 1);
                e.component.reload();
            },
            onReorder({
                fromIndex,
                toIndex,
                fromData,
                component,
            }) {
                [fromData[fromIndex], fromData[toIndex]] = [fromData[toIndex], fromData[fromIndex]];
                component.reload();
            },
        },
    }).dxList('instance');
}

// Инициализация кнопки для расчета маршрута
function initRecalculateWaypointsButton() {
    $('#recalculateWaypointsButton').dxButton({
        stylingMode: 'contained',
        text: 'Рассчитать маршрут',
        type: 'default',
        width: 600,
        height: 40,
        onClick() {
            recalculateWaypoints(selectedPointsWidget.option('items'));
        },
    });
}

// Отображение маршрута
function showTravelPoints(indexes) {
    const points = waypoints.getSelectedPoints(indexes);
    let calculatedPoints = [];
    let prevPoint = null;
    let id = 0;
    let total_distance = 0;
    for (const idx in points) {
        const point = points[idx];
        if (prevPoint != null) {
            const d = calculateDistance(point.location[0], point.location[1], prevPoint.location[0], prevPoint.location[1]) / 1000.0;
            total_distance += d;
            calculatedPoints.push({
                ID: id,
                Waypoint: d.toFixed(2) + ' км.',
                type: 'distance'
            });
            id++;
        }

        calculatedPoints.push({
            ID: id,
            Waypoint: point.text,
            type: 'pointName'
        });
        id++;

        prevPoint = point;
    }
    calculatedPoints.push({
        ID: id,
        Waypoint: "Общая длина маршрута: " + total_distance.toFixed(2) + ' км.',
        type: 'distance'
    });
    $(() => {
        $('#waypointsGrid').dxList({
            dataSource: calculatedPoints,
            height: 400,
            width: '100%',
            showScrollbar: 'always',
            keyExpr: 'ID',
            showBorders: true,
            pageLoadMode: "scrollBottom",
            paginate: false,
            itemTemplate(data) {
                const result = $('<div>').addClass(data.type).text(data.Waypoint);
                return result;
            }
        });
    });
}

// Расчет расстояние между двумя точками заданными координатами на поверхности Земли
function calculateDistance(lat1, lon1, lat2, lon2) {
    const R = 6371e3; // metres
    const φ1 = lat1 * Math.PI / 180; // φ, λ in radians
    const φ2 = lat2 * Math.PI / 180;
    const Δφ = (lat2 - lat1) * Math.PI / 180;
    const Δλ = (lon2 - lon1) * Math.PI / 180;
    const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
        Math.cos(φ1) * Math.cos(φ2) *
        Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    const d = R * c; // in metres
    return d;
}

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function shuffle(array, fix_start_point = true, fix_end_point = true) {
    let currentIndex = array.length - 1;
    if (fix_end_point) {
        currentIndex = array.length - 2;
    }
    let start_index = 0;
    if (fix_start_point) {
        start_index = 1;
    }
    while (currentIndex !== start_index) {
        let right = currentIndex;
        let left = getRandomInt(start_index, currentIndex);
        // And swap it with the current element.
        [array[left], array[right]] = [
            array[right], array[left]
        ];
        currentIndex--;
    }
}

// Факториал
function factorial(n) {
    j = 1;
    for (i = 1; i <= n; i++) {
        j = j * i;
    }
    return j;
}


function stochasticPathFind(array, fix_start_point = true, fix_end_point = true) {
    let variants_count = array.length;
    if (fix_start_point) {
        variants_count--;
    }
    if (fix_end_point) {
        variants_count--;
    }
    let max_ways = factorial(array.length);
    if (max_ways > max_ways_count) {
        max_ways = max_ways_count;
    }
    var min_distance = Number.MAX_VALUE;
    var min_distance_waypoint = [];
    for (let i = 0; i < max_ways; i++) {
        shuffle(array, fix_start_point, fix_end_point);
        let prevPoint = null;
        let distance = 0;
        for (let ind in array) {
            let point = array[ind];
            if (prevPoint != null) {
                const d = calculateDistance(point.location[0], point.location[1], prevPoint.location[0], prevPoint.location[1]) / 1000.0;
                distance += d;
            }
            prevPoint = point;
        }
        if (distance < min_distance) {
            min_distance = distance;
            min_distance_waypoint = [];
            for (let ind in array) {
                min_distance_waypoint.push(array[ind].id);
            }
        }
    }
    return min_distance_waypoint;
}

// Вычисление оптимального маршрута по начальной и конечной точке
function recalculateWaypoints(waypoints) {
    if (!waypoints || waypoints.length < 1) {
        DevExpress.ui.notify("Недостаточно точек для построения маршрута");
    } else if (waypoints.length == 1) {

        // Одна точка, просто ее выводим
        showTravelPoints([waypoints[0].id]);
        reInitMapByPoints([waypoints[0].id]);
    } else {
        var start = waypoints[0];
        var end = waypoints[waypoints.length - 1];
        DevExpress.ui.notify("Расчет маршрута от '" + start.text + "' к '" + end.text + "'");
        if (waypoints.length == 2) {

            // Две точки, старт и стоп, выводим без расчета
            showTravelPoints([waypoints[0].id, waypoints[1].id]);
            reInitMapByPoints([waypoints[0].id, waypoints[1].id]);
        } else if (waypoints.length == 3) {

            // Три точки, старт, стоп и промежуточная выводим без расчета
            showTravelPoints([waypoints[0].id, waypoints[1].id, waypoints[2].id]);
            reInitMapByPoints([waypoints[0].id, waypoints[1].id, waypoints[2].id]);
        } else {

            // применяем лучшие поиск из 200.000 лучших вариантов (9! = 362880, берем такого порядка число чтобы не грузить браузер расчетами)
            let path_indexes = stochasticPathFind(waypoints, true, true);
            showTravelPoints(path_indexes);
            reInitMapByPoints(path_indexes);
        }
    }
}

$(() => {
    waypoints.loadData();
    initMap();
    initRegionDropDown();
    initRecalculateWaypointsButton();
});