/**
 * CO2Calculator.js 1.0.0
 * Description
 *
 * Copyright 2020, Avidly Denmark ApS
 * https://www.avidlyagency.com/
 *
 * Licensed under MIT
 *
 * Released on: Marts, 2020
 */

(function () {
    'use strict';

    let self, autocompleteOrigin, autocompleteDestination, terminalRoutes, vesselRoutes, vesselPorts, csvFiles, co2Factors;
    const vesselPortsAddresses = [];
    const calculatedRoutes = { unifeeder: [], alternative: [] };

    // Define constructor
    this.CO2Calculator = function () {
        self = this;

        // Define option defaults
        const defaults = {
            selectors: {
                origin: null,
                destination: null,
                pol: null,
                pod: null,
                containers: null,
                submit: null,
            },
            co2Factors: {
                truck: 62,
                vessel: 51.66,
                rail: 22,
                el_rail: 22,
                barge: 33.33
            },
            containerWeight: 15000,
            googleMapsAPI: "",
            restrictedCountries: [],
            on: {
                submit: null,
                success: null,
                error: null
            },
            onCompleted: null
        };

        // Create options by extending defaults with the passed in arguments
        if (arguments[0] && typeof arguments[0] === "object") {
            co2Factors = extendDefaults(defaults.co2Factors, arguments[0].co2Factors);
            self.options = extendDefaults(defaults, arguments[0]);
        }

        csvFiles = {
            vesselPorts: this.options.filesPath + "vessel-ports.csv",
            vesselRoutes: this.options.filesPath + "vessel-routes.csv",
            terminalRoutes: this.options.filesPath + "terminal-routes.csv"
        }

        document.addEventListener("onSubmit", function (e) {
            if (typeof self.options.on.submit === "function") {
                self.options.on.submit(e.detail.button);
            }
        });

        document.addEventListener("onSuccess", function (e) {
            if (typeof self.options.on.success === "function") {
                self.options.on.success(e.detail.result);
            }
        });

        document.addEventListener("onError", function (e) {
            if (typeof self.options.on.error === "function") {
                self.options.on.error(e.detail.message);
            }
        });

        self.options.selectors.submit.addEventListener("click", function () {
            triggerEvent("onSubmit", {button: this});

            // Reset routes object container
            calculatedRoutes.unifeeder = [];
            calculatedRoutes.alternative = [];

            // Calculate route by truck for alternative route.
            calculateRouteByTruck(self.options.selectors.origin.value, self.options.selectors.destination.value, function (distance) {
                calculatedRoutes.alternative.push({
                    transport: "truck",
                    origin: self.options.selectors.origin.value,
                    destination: self.options.selectors.destination.value,
                    distance: distance,
                    co2: getCalculateCO2Emission("truck", distance)
                });
            });

            // Next calculate Unifeeder route.
            calculateUnifeederRoute();
        });

        getDataFromLocalCSV(csvFiles.terminalRoutes, {}, function (data) {
            terminalRoutes = data;
        });

        getDataFromLocalCSV(csvFiles.vesselRoutes, [], function (data) {
            vesselRoutes = data;
            if(self.options.selectors.pol && self.options.selectors.pod) setupAutocompletePortInputs();
        });

        getDataFromLocalCSV(csvFiles.vesselPorts, {}, function (data) {
            vesselPorts = data;

            const ports = Object.values(vesselPorts);
            for (let i = 0; i < ports.length; i++) {
                vesselPortsAddresses.push(ports[i].address);
            }
        });

        loadGoogleMapsAPI();
    };

    // Extend defaults with user options
    function extendDefaults(source, properties) {
        let property;
        for (property in properties) {
            if (properties.hasOwnProperty(property)) {
                source[property] = properties[property];
            }
        }
        return source;
    }

    function loadGoogleMapsAPI() {
        const existingMapsScript = document.getElementById("google-maps-api");

        if (!existingMapsScript) {
            const script = document.createElement("script");
            script.id = "google-maps-api";
            script.src = "https://maps.googleapis.com/maps/api/js?key=" + self.options.googleMapsAPI + "&libraries=places&language=en";
            document.body.appendChild(script);

            script.onload = function () {
                setupAutocompleteOnInputs();
            }
        }

        if (existingMapsScript) setupAutocompleteOnInputs();
    }

    function setupAutocompleteOnInputs() {
        const options = {
            componentRestrictions: {country: self.options.restrictedCountries}
        };

        autocompleteOrigin = new google.maps.places.Autocomplete(self.options.selectors.origin, options);
        autocompleteDestination = new google.maps.places.Autocomplete(self.options.selectors.destination, options);
    }

    function setupAutocompletePortInputs() {
        const pol = self.options.selectors.pol;
        const pod = self.options.selectors.pod;

        const vesselRoutesArray = Object.keys(vesselRoutes);
        let options = `<option value="" selected>${pol.dataset.placeholder ?? ""}</option>`;
        for (let i = 0; i < vesselRoutesArray.length; i++) options += `<option value="${vesselRoutesArray[i]}">${vesselRoutesArray[i]}</option>`;
        pol.innerHTML = options;

        pod.innerHTML = `<option value="" selected>${pod.dataset.placeholder ?? ""}</option>`;
        pod.disabled = true;

        pol.addEventListener('change', function () {
            const key = Object.keys(vesselRoutes)[pol.options.selectedIndex - 1];
            const routes = vesselRoutes[key] ?? [];

            let options = `<option value="" selected>${pod.dataset.placeholder ?? ""}</option>`;
            for (let i = 0; i < routes.length; i++) options += `<option value="${routes[i].arrival}">${routes[i].arrival}</option>`;
            pod.innerHTML = options;
            pod.disabled = pol.options.selectedIndex === 0;
        });
    }

    function calculateUnifeederRoute() {
        const place = autocompleteOrigin.getPlace();
        const latlng = {lat: place.geometry.location.lat(), lng: place.geometry.location.lng()};

        getCountryAndPostalCodes(autocompleteOrigin, function (codes) {
            const {countryCode, postalCode} = codes;

            getDataFromLocalCSV(self.options.filesPath + "postal-codes/" + countryCode + ".csv", {}, function (data) {
                const route = data[postalCode];

                if (route === undefined) return triggerEvent("onError", {message: "Postal code " + postalCode + " doesn't exist in " + countryCode + ".csv."});

                const pol = self.options.selectors.pol?.options[self.options.selectors.pol?.options.selectedIndex].value;

                if(pol) {
                    const polPossiblePorts = Object.keys(terminalRoutes).filter(key => terminalRoutes[key].port === pol);

                    let polPossibleAddresses = polPossiblePorts.map(port => terminalRoutes[port].address);
                    polPossibleAddresses.unshift(vesselPorts[pol].address);

                    getNearestPortIndexByAddress(latlng, polPossibleAddresses, function (nearestPortIndex) {
                        if(nearestPortIndex > 0) {
                            const portCode = polPossiblePorts[nearestPortIndex - 1];
                            addStepsToUnifeederRoute(latlng, terminalRoutes[portCode], portCode, pol);
                        } else {
                            addStepsToUnifeederRoute(latlng, vesselPorts[pol], false, pol);
                        }
                    });
                } else {

                    // Does the route contain pre-carriage
                    if (route.terminal && terminalRoutes[route.terminal]) {
                        const truckDestination = terminalRoutes[route.terminal];
                        addStepsToUnifeederRoute(latlng, truckDestination, route.terminal, truckDestination.port);
                    } else {
                        getNearestPortIndexByAddress(latlng, vesselPortsAddresses, function (nearestPortIndex) {
                            const portCode = Object.keys(vesselPorts)[nearestPortIndex];
                            addStepsToUnifeederRoute(latlng, vesselPorts[portCode], false, portCode);
                        });
                    }

                }
            });
        });
    }

    function getCountryAndPostalCodes(autocompleteField, callback) {
        const place = autocompleteField.getPlace();
        const latlng = {lat: place.geometry.location.lat(), lng: place.geometry.location.lng()};

        const geocoder = new google.maps.Geocoder;
        geocoder.geocode({'location': latlng}, function (results, status) {
            if (status !== "OK") triggerEvent("onError", {message: status});

            const addressComponents = results[0].address_components;

            let countryCode = addressComponents.reduce((acc, line) => line.types.includes("country") ? line.short_name.toLowerCase() : acc, {});
            let postalCode = addressComponents.reduce((acc, line) => line.types.includes("postal_code") ? line.long_name : acc, null);

            if(!postalCode) return triggerEvent("onError", {message: "Can't find postal code from: " + place.formatted_address});

            if (countryCode === "gb" || countryCode === "pt") {
                postalCode = postalCode.split(' ')[0];
            } else {
                postalCode = postalCode.replace(/\D/g, '');
            }

            return callback({countryCode, postalCode});
        });
    }

    function addStepsToUnifeederRoute(trackOrigin, truckDestination, addPreCarriage, departPortCode) {
        const allVesselRoutes = vesselRoutes[departPortCode], possibleVesselRoutes = [], possibleVesselRoutesAddresses = [];

        if (allVesselRoutes === undefined) return triggerEvent("onError", {message: "Port " + departPortCode + " doesn't exist in vessel-routes.csv."});
        if (vesselPorts[departPortCode] === undefined) return triggerEvent("onError", {message: "Port " + departPortCode + " doesn't exist in vessel-ports.csv."});

        getCountryAndPostalCodes(autocompleteDestination, function (codes) {
            const {countryCode, postalCode} = codes;

            getDataFromLocalCSV(self.options.filesPath + "/postal-codes/" + countryCode + ".csv", {}, function (data) {
                const onCarriageRoute = data[postalCode];

                if (onCarriageRoute === undefined) return triggerEvent("onError", {message: "Postal code " + postalCode + " doesn't exist in " + countryCode + ".csv."});

                const pod = self.options.selectors.pod?.options[self.options.selectors.pod?.options.selectedIndex].value;
                if(pod) {
                    const pol = self.options.selectors.pol?.options[self.options.selectors.pol?.options.selectedIndex].value;
                    const vesselRoute = vesselRoutes[pol].reduce((acc, port) => pod === port.arrival ? port : acc, null);
                    possibleVesselRoutes.push(vesselRoute);
                    possibleVesselRoutesAddresses.push(vesselPorts[pol].address);

                } else {

                    // Find possible vessel routes inside same country
                    for (let p = 0; p < allVesselRoutes.length; p++) {
                        const vesselPort = vesselPorts[allVesselRoutes[p].arrival];

                        if (vesselPort === undefined) return triggerEvent("onError", {message: "Port " + allVesselRoutes[p].arrival + " doesn't exist in vessel-ports.csv"});
                        if (vesselPort.country.toUpperCase() !== countryCode.toUpperCase()) continue;

                        possibleVesselRoutes.push(allVesselRoutes[p]);
                        possibleVesselRoutesAddresses.push(vesselPort.address);
                    }

                    // If we can't find and possible vessel routes inside same country, expand to search in all vessel routes from that port.
                    if(possibleVesselRoutes.length === 0) {

                        const port = allVesselRoutes.reduce((acc, port) => terminalRoutes[onCarriageRoute.terminal]?.port === port.arrival ? port : acc, null);

                        // Is there on-carriage?
                        if(port) {
                            possibleVesselRoutes.push(port);
                            possibleVesselRoutesAddresses.push(vesselPorts[port.arrival].address);
                        } else {
                            for (let p = 0; p < allVesselRoutes.length; p++) {
                                possibleVesselRoutes.push(allVesselRoutes[p]);
                                possibleVesselRoutesAddresses.push(vesselPorts[allVesselRoutes[p].arrival].address);
                            }
                        }
                    }

                }

                getNearestPortIndexByAddress(self.options.selectors.destination.value, possibleVesselRoutesAddresses, function (nearestPortIndex) {
                    const nearestPort = possibleVesselRoutes[nearestPortIndex];
                    const addOnCarriageRoute = onCarriageRoute.terminal && terminalRoutes[onCarriageRoute.terminal].port === nearestPort.arrival;
                    const truckOriginAddress = addOnCarriageRoute ? terminalRoutes[onCarriageRoute.terminal].address : vesselPorts[nearestPort.arrival].address;

                    // Calculate first truck route
                    calculateRouteByTruck(trackOrigin, truckDestination.address, function (distance) {

                        // Add first truck route to unifeeder array
                        calculatedRoutes.unifeeder.push({
                            transport: "truck",
                            origin: self.options.selectors.origin.value,
                            destination: truckDestination.address,
                            distance: distance,
                            type: "km",
                            co2: getCalculateCO2Emission("truck", distance)
                        });

                        // Add rail route if necessary
                        if (addPreCarriage) {
                            const transport = (truckDestination.electriefied === "YES") ? "el_rail" : truckDestination.mode.toLowerCase();
                            calculatedRoutes.unifeeder.push({
                                transport: truckDestination.mode.toLowerCase(),
                                origin: truckDestination.address,
                                destination: vesselPorts[departPortCode].address,
                                distance: truckDestination.distance,
                                type: "km",
                                co2: getCalculateCO2Emission(transport, truckDestination.distance)
                            });
                        }

                        // Add vessel route
                        calculatedRoutes.unifeeder.push({
                            transport: "vessel",
                            origin: vesselPorts[departPortCode].address + " (" + departPortCode + ")",
                            destination: vesselPorts[nearestPort.arrival].address + " (" + nearestPort.arrival + ")",
                            distance: nearestPort.distance * 0.6214,
                            type: "miles",
                            co2: getCalculateCO2Emission("vessel", nearestPort.distance)
                        });

                        // Does the route contain on-carriage
                        if (addOnCarriageRoute) {
                            const railDetails = terminalRoutes[onCarriageRoute.terminal];

                            // const transport = (railDetails.electriefied === "YES") ? "el_rail" : truckDestination.mode.toLowerCase();
                            const transport = (railDetails.electriefied === "YES") ? "el_rail" : railDetails.mode.toLowerCase();
                            calculatedRoutes.unifeeder.push({
                                transport: railDetails.mode.toLowerCase(),
                                origin: vesselPorts[nearestPort.arrival].address,
                                destination: railDetails.address,
                                distance: railDetails.distance,
                                type: "km",
                                co2: getCalculateCO2Emission(transport, railDetails.distance)
                            });
                        }

                        // Add last truck route and end calculation
                        calculateRouteByTruck(truckOriginAddress, self.options.selectors.destination.value, function (data) {
                            calculatedRoutes.unifeeder.push({
                                transport: "truck",
                                origin: truckOriginAddress,
                                destination: self.options.selectors.destination.value,
                                distance: data,
                                type: "km",
                                co2: getCalculateCO2Emission("truck", data)
                            });

                            triggerEvent("onSuccess", {result: calculatedRoutes});
                        });
                    });
                });
            });
        });
    }

    function getNearestPortIndexByAddress(origin, portAddresses, callback) {
        const addressChunks = [], addresses = [], increase = 25;
        let indexedAddresses = 0, increaseIndex = 0;

        for (let i = 0; i < portAddresses.length; i += increase) {
            addressChunks.push(portAddresses.slice(i, i + increase));
        }

        for (let i = 0; i < addressChunks.length; i++) {
            getNearestAddress(origin, addressChunks[i], increaseIndex, function (routes) {
                indexedAddresses += routes.length;

                for (let r = 0; r < routes.length; r++) {
                    addresses.push(routes[r]);
                }

                if (indexedAddresses === portAddresses.length) {
                    addresses.sort(sortByDistDM);

                    const routesPriority = [];
                    for (let a = 0; a < addresses.length; a++) {
                        routesPriority.push(addresses[a].index);
                    }

                    return callback(routesPriority[0]);
                }
            });

            increaseIndex += increase;
        }
    }

    function getNearestAddress(origin, addresses, startIndex, callback) {
        const distanceMatrixService = new google.maps.DistanceMatrixService();
        const matrixRequest = {
            origins: [origin],
            destinations: addresses,
            travelMode: google.maps.TravelMode.DRIVING,
            avoidFerries: true
        };

        distanceMatrixService.getDistanceMatrix(matrixRequest, function (response, status) {
            if (status !== "OK") triggerEvent("onError", {message: status});

            const routes = response.rows[0].elements;
            for (let i = 0; i < routes.length; i++) {
                const route = routes[i];
                route.index = startIndex + i;
            }

            return callback(routes);
        });
    }

    function calculateRouteByTruck(origin, destination, callback) {
        const directionsService = new google.maps.DirectionsService();
        const request = {
            origin: origin,
            destination: destination,
            travelMode: google.maps.TravelMode.DRIVING,
            avoidFerries: true
        };

        directionsService.route(request, function (response, status) {
            if (status !== "OK") return triggerEvent("onError", {message: "Can't find any route between " + origin + " and " + destination});

            const distance = response.routes[0].legs[0].distance.value / 1000;
            callback(distance);
        });
    }

    function sortByDistDM(a, b) {
        return (a.distance.value - b.distance.value)
    }

    function getCalculateCO2Emission(transport, distance) {
        const payload = self.options.selectors.containers.value * self.options.containerWeight || 0
        return (((distance * payload) / 1000) * co2Factors[transport]) / 1000;
    }

    function triggerEvent(eventName, data) {
        let event;
        if (window.CustomEvent) {
            event = new CustomEvent(eventName, {detail: data});
        } else {
            event = document.createEvent('CustomEvent');
            event.initCustomEvent(eventName, true, true, {detail: data});
        }
        document.dispatchEvent(event);
    }

    function getDataFromLocalCSV(url, subDataType, callback) {
        const request = new XMLHttpRequest();
        request.open("GET", url, true);

        request.onload = function () {
            const lines = request.responseText.split(/\r\n|\n/);
            const headers = lines[0].split(';');
            const result = {};

            for (let i = 1; i < lines.length; i++) {
                const data = lines[i].split(';');

                if (data.length === headers.length) {
                    const line = {};

                    for (let j = 1; j < headers.length; j++) {
                        line[headers[j].toLowerCase()] = isNaN(data[j]) ? data[j] : Number(data[j]);
                    }

                    if (Array.isArray(subDataType)) {
                        if (!(data[0] in result)) result[data[0]] = [];
                        result[data[0]].push(line);
                    } else {
                        result[data[0]] = line;
                    }
                }
            }

            return callback(result);
        };

        request.send();
    }

}).call(this);