/*
 * Copyright 2021 - All rights reserved.
 * Rodrigo Tapia-McClung
 *
 * January 2021
 */

/* globals omnivore, Promise, makeBaseMap, am4core, makeRadarChart, makeLineChart, makeAreaChart, makeBarChart, makeDateRange */
/* exported. userFiles, userDates, timeParse, layerControl, yearList */

let timeParse,
    timeFormat,
    timeDimensionControl,
    userFiles = [],
    monthArray = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
    dateArray = [],
    dateMin,
    dateMax,
    minUserDate,
    maxUserDate,
    userDates,
    map,
    overlay,
    glmap,
    osmLayer, cartoLightLayer, cartoDarkLayer,
    timeLayer,
    layerControl;


let colors = {
    "Agua permanente": {
        "fill": "#00c5ff"
    },
    "Áreas temporalmente inundadas": {
        "fill": "#ff5500"
    },
    "Suelos húmedos-vegetación acuática": {
        "fill": "#98e600"
    },
    "Capa base G3WBM": {
        "fill": "#aaaaaa"
    }
}

let currentTiles = {},
    allTiles = {};

d3.json("https://unpkg.com/d3-time-format@2.1.1/locale/es-MX.json").then(async locale => {

    d3.timeFormatDefaultLocale(locale);

    timeParse = d3.timeParse("%B_%Y");
    timeFormat = d3.timeFormat("cuerpos_%B_%Y");

    setupTimeDimensionControl();
    let dates = await setupDates(),
        userData = await populateDates(dates),
        mapData = await setupMap(userData);
    populateMap(mapData);
    makeCharts();
});

const sortInitialDateAscending = (a, b) =>  a - b;
// Dates will be cast to numbers automagically:

const windowUrl = new URL(window.location.href);
const baseUrl = windowUrl.hostname == "localhost" ?
    `${windowUrl.origin}` : // returns "http://localhost:8090"
    `${windowUrl.origin}/fordecyt_2019`; // returns "http://servername/fordecyt_2019"
// use `${baseUrl}/something`

// query available dates on DB
const setupDates = () => {
    return new Promise(resolve => {
        let layersQuery = `${baseUrl}/list_layers`;
        d3.json(layersQuery).then(layers => {
            layers.forEach(layer => {
                if (layer.f_table_name.startsWith("cuerpos")) {
                    let table = layer.f_table_name.split("cuerpos_")[1];
                    dateArray.push(timeParse(table)); // convert filenames to dates
                }
            });

            dateArray = dateArray.sort(sortInitialDateAscending); // order dates
            dateMin = d3.min(dateArray);
            dateMax = d3.max(dateArray);

            const dates = { min: dateMin, max: dateMax, dates: dateArray };
            resolve(dates);
        });
    });
}

const populateDates = (dates) => { // fill out date pickers with available dates
    return new Promise(resolve => {
        $.datepicker.regional["es"] = {
            closeText: "Cerrar",
            prevText: "&#x3c;Ant",
            nextText: "Sig&#x3e;",
            currentText: "Hoy",
            monthNames: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
                "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
            ],
            monthNamesShort: ["Ene", "Feb", "Mar", "Abr", "May", "Jun",
                "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"
            ],
            dayNames: ["Domingo", "Lunes", "Martes", "Mi&eacute;rcoles", "Jueves", "Viernes", "S&aacute;bado"],
            dayNamesShort: ["Dom", "Lun", "Mar", "Mi&eacute;", "Juv", "Vie", "S&aacute;b"],
            dayNamesMin: ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "S&aacute;"],
            weekHeader: "Sm",
            dateFormat: "yy/mm/dd",
            firstDay: 1,
            isRTL: false,
            showMonthAfterYear: false,
            yearSuffix: ""
        }
        $.datepicker.setDefaults($.datepicker.regional["es"]);

        // month pickers
        $("#date-initial").datepicker({
            minDate: dates.min,
            maxDate: dates.max,
            defaultDate: dates.min,
            changeMonth: true,
            changeYear: true,
            showButtonPanel: true,
            dateFormat: "M yy",
            onClose: function() {
                let month = $("#ui-datepicker-div .ui-datepicker-month :selected").val();
                let year = $("#ui-datepicker-div .ui-datepicker-year :selected").val();
                minUserDate = new Date(year, month, 1); // initial date
                $(this).datepicker("setDate", minUserDate);
                $("#date-final").datepicker("option", {
                    "minDate": minUserDate,
                    disabled: false
                });
                // hack to avoid needing to change date twice in second datepicker
                setTimeout(() => { $("#date-final").datepicker("show") }, 10);
            },
            beforeShow: el => {
                $("#ui-datepicker-div").toggleClass("hide-calendar", $(el).is("[data-calendar=\"false\"]"));
            }
        });

        $("#date-final").datepicker({
            maxDate: dates.max,
            defaultDate: dates.max,
            changeMonth: true,
            changeYear: true,
            showButtonPanel: true,
            disabled: true,
            dateFormat: "M yy",
            onClose: function() {
                let month = $("#ui-datepicker-div .ui-datepicker-month :selected").val();
                let year = $("#ui-datepicker-div .ui-datepicker-year :selected").val();
                maxUserDate = new Date(year, month, 1); // final date
                $(this).datepicker("setDate", maxUserDate);
                // use .setUTCHours(6,0,0) to adjust DST offset fror some months
                let startUserDate = new Date(minUserDate.setUTCHours(6, 0, 0));
                let endUserDate = new Date(maxUserDate.setUTCHours(6, 0, 0));

                // pass new timeinterval to timeDimension player
                userDates = L.TimeDimension.Util.explodeTimeRange(startUserDate, endUserDate, "P1M");
                userFiles = userDates.map(month => timeFormat(month)); // order table names by date

                // change date selectors from main screen to map
                $("#date-initial").detach().appendTo("#datePickers");
                $("#date-final").detach().appendTo("#datePickers");
                $("#datePickers").css("display", "block");

                // hide initial screen and show map
                $("#startHeader").remove();
                $("#initial-backdrop").remove();
                $("#indicatorSelect").parent().remove();
                $("#mainContainer")[0].style.setProperty("display", "flex", "important");
                $("#mexmap").show();

                // When closing final-date, either setup or update map
                if (!map) {
                    resolve({ min: startUserDate, max: endUserDate });
                } else {
                    // FIXME: no need to pass data here?
                    updateMap({ map: map, min: startUserDate, max: endUserDate });
                    updateCharts();
                }
            },
            beforeShow: (el, inst) => {
                inst.input.datepicker("refresh");
                $("#ui-datepicker-div").toggleClass("hide-calendar", $(el).is("[data-calendar=\"false\"]"));
            }
        });
    });
}

const setupMap = (dates) => {
    return new Promise(resolve => {

        // make body tag to have style height: 100%
        $("body").css("height", "100%");
        osmLayer = L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
            maxZoom: 19,
            attribution: "&copy; <a href=\"http://osm.org/copyright\" target=\"_blank\">OpenStreetMap</a> contributors"
        });
        cartoLightLayer = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", {
            maxZoom: 19,
            attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
        });
        cartoDarkLayer = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", {
            maxZoom: 19,
            attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
        });

        //let bounds = cuencaBufferMask.getBounds();
        let southWest = L.latLng(15.08, -94.42),
            northEast = L.latLng(18.82, -91.52),
            bounds = L.latLngBounds(southWest, northEast);

        map = L.map("mexmap", {
            //center: [17.22, -92.28],
            minZoom: 7, //8
            maxZoom: 18,
            zoom: 7, //6
            attributionControl: false,
            timeDimension: true,
            timeDimensionOptions: {
                times: userDates,
                currentTime: dates.min
            },
            maxBounds: bounds
        }).setView([16.96, -92.97], 8);

        cartoDarkLayer.addTo(map);

        // set sync map on right
        let osmLayerOverlay = L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
                maxZoom: 19,
                attribution: "&copy; <a href=\"http://osm.org/copyright\" target=\"_blank\">OpenStreetMap</a> contributors"
            }),
            cartoLightLayerOverlay = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", {
                maxZoom: 19,
                attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
            }),
            cartoDarkLayerOverlay = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", {
                maxZoom: 19,
                attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
            });

        overlay = L.map("overlaydiv", {
            zoomControl: false,
            inertia: false,
            keyboard: false,
            //dragging: false,
            scrollWheelZoom: true,
            attributionControl: false,
            zoomAnimation: true
        }).setView([16.96, -92.97], 8);

        cartoDarkLayerOverlay.addTo(overlay);
        L.control.attribution({ position: "bottomright" }).addTo(overlay);

        // on base layer on main map, change base layer on overlay map
        map.on("baselayerchange", event => {
            overlay.eachLayer(l => overlay.removeLayer(l));
            if (event.name == "Carto Dark") {
                cartoDarkLayerOverlay.addTo(overlay);
            } else if (event.name == "Carto Light") {
                cartoLightLayerOverlay.addTo(overlay);
            } else if (event.name == "OpenStreetMap") {
                osmLayerOverlay.addTo(overlay);
            }
        });

        const offsetGlobal = (center, zoom, refMap, tgtMap) => {
            let refC = refMap.getContainer();
            let tgtC = tgtMap.getContainer();
            let pt = refMap.project(center, zoom)
                .subtract([refC.offsetLeft, refC.offsetTop])
                .subtract(refMap.getSize().divideBy(2))
                .add([tgtC.offsetLeft, tgtC.offsetTop])
                .add(tgtMap.getSize().divideBy(2));
            return refMap.unproject(pt, zoom);
        }

        map.sync(overlay, { offsetFn: offsetGlobal });
        resolve({ "map": map });
    });
}

const populateMap = async (mapData) => {

    let map = mapData.map;

    let cuencaLayer = L.geoJson(null, {
        style: {
            stroke: true,
            weight: 5,
            color: "grey",
            opacity: .35,
            fillOpacity: 0.05
        },
        interactive: false
    });
    let cuencaBufferMask = omnivore.topojson("data/buffer_cuenca.topojson", null, cuencaLayer);
    cuencaBufferMask.addTo(map);

    // create mvt layers
    userFiles.forEach(f => {
        mapboxLayer(f);
    });

    glmap = L.mapboxGL({
        "accessToken": "no-token",
        "style": {
            "version": 8,
            "sources": {},
            "layers": []
        }
    }).addTo(map);

    // after mapboxGL map is ready with styles do this:
    glmap.getMapboxMap().on("style.load", () => {

        glmap.getMapboxMap().addSource("basecuerposagua", {
            "type": "vector",
            "tiles": [`${baseUrl}/basecuerposagua/mvt/{z}/{x}/{y}?geom_column=geom&columns=descrip`],
            "maxzoom": 14,
            "minzoom": 7
        });

        glmap.getMapboxMap().addLayer({
            "id": "baseFill",
            "source": "basecuerposagua",
            "source-layer": "basecuerposagua",
            "type": "fill",
            "paint": {
                "fill-opacity": 0.25,
                "fill-color": "#aaaaaa"
            }
        });

        glmap.getMapboxMap().addLayer({
            "id": "baseLine",
            "source": "basecuerposagua",
            "source-layer": "basecuerposagua",
            "type": "line",
            "paint": {
                "line-opacity": 0.75,
                "line-width": ["interpolate", ["linear"],
                    ["zoom"],
                    8, 0,
                    18, 4
                ],
                "line-color": ["match", ["get", "descrip"],
                    "Áreas temporalmente inundadas", "#ff5500",
                    "Suelos húmedos-vegetación acuática", "#98e600",
                    "#00c5ff" // Agua permanente default
                ],
                "line-dasharray": [2, 2]
            }
        });

        userFiles.forEach(monthYear => {
            glmap.getMapboxMap().addLayer(currentTiles[monthYear]);
        });

        Object.keys(allTiles).forEach(layer => {
            if (layer == userFiles[0]) {
                glmap.getMapboxMap().setPaintProperty(layer, "fill-opacity", 0.25);
            }
        });

        timeDimensionControl.addTo(map);
        // Pass dummy geojson layer to timeDimension in order to register and sync
        timeLayer = L.timeDimension.layer.Tile(L.geoJSON(), {
            updateTimeDimension: true,
            updateTimeDimensionMode: "replace",
            waitForReady: true,
            duration: "P1M"
        });
        timeLayer.addTo(map);
        legend.addTo(map);

        let baseLayers = {
            "Carto Dark": cartoDarkLayer,
            "Carto Light": cartoLightLayer,
            "OpenStreetMap": osmLayer
        };
        let overlays = {
            "<span id=\"cuencaOverlay\">Agua en la cuenca del Grijalva</span>": timeLayer
        };

        layerControl = L.control.layers(baseLayers, overlays).addTo(map);
        makeBaseMap(); // basemap.js

        // fix for leaflet-mapbox-gl v. 0.0.11 that adds map's to tile pane:
        // get children of map tile pane, create array from it and iterate
        // setting their z-index
        let glmapChildren = map.getPanes().tilePane.children,
            children = Array.from(glmapChildren);
        children.forEach(c => c.style.zIndex = "inherit");
    });
}

const updateMap = (mapData) => {

    // clear tiles
    currentTiles = {};
    //retrieve or create tiles for current dates
    userFiles.forEach(monthYear => {
        if (Object.keys(allTiles).includes(monthYear)) {
            currentTiles[monthYear] = allTiles[monthYear]; // recover tile if it has already been created
            return; // if file has already been processed, exit
        } else { // if file cannot be found in allTiles, then add 1 to the number of files to process
            let newTile = mapboxLayer(monthYear);
            glmap.getMapboxMap().addLayer(newTile);
            if (monthYear == userFiles[0]) {
                glmap.getMapboxMap().setPaintProperty(monthYear, "fill-opacity", 0.7);
            }
        }
    });

    // update timeDimension times
    timeLayer._timeDimension.setAvailableTimes(userDates, "replace");
    timeLayer._timeDimension.setCurrentTime(mapData.min);
}

// define MVT layer for given month table and description
const mapboxLayer = (monthYear) => {

    let pbfLayer = {
        "id": monthYear,
        "source": {
            "type": "vector",
            "tiles": [`${baseUrl}/${monthYear}/mvt/{z}/{x}/{y}?geom_column=geom&columns=descrip`],
            "maxzoom": 14,
            "minzoom": 7
        },
        "source-layer": monthYear,
        "type": "fill",
        "paint": {
            "fill-opacity": 0,
            "fill-color": ["match", ["get", "descrip"],
                "Áreas temporalmente inundadas", "#ff5500",
                "Suelos húmedos-vegetación acuática", "#98e600",
                "#00c5ff" // Agua permanente default
            ]
        }
    }
    currentTiles[monthYear] = pbfLayer;
    allTiles[monthYear] = pbfLayer;
    return pbfLayer;
}

const setupTimeDimensionControl = () => {
    L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
        _getDisplayDateFormat: date => {
            let d = new Date(date);
            let year = d.getFullYear().toString();
            let month = d.getUTCMonth();
            return `${monthArray[month]} ${year}`;
        }
    });

    timeDimensionControl = new L.Control.TimeDimensionCustom({
        loopButton: true,
        /*minSpeed: 1,
        maxSpeed: 5,*/
        timeSteps: 1,
        playReverseButton: true,
        //limitSliders: true,
        playerOptions: {
            //buffer: 5,
            //minBufferReady: 5,
            transitionTime: 125,
            loop: true
        },
        timeZones: ["Local"]
    });
}

L.TimeDimension.Layer.Tile = L.TimeDimension.Layer.extend({
    _setAvailableTimes: function() {
        if (this.options.times) {
            return L.TimeDimension.Util.parseTimesExpression(this.options.times);
        } else if (this.options.timeInterval) {
            let tiArray = L.TimeDimension.Util.parseTimeInterval(this.options.timeInterval);
            let period = this.options.period || "P1D";
            let validTimeRange = this.options.validTimeRange || undefined;
            //alert("times");
            return L.TimeDimension.Util.explodeTimeRange(tiArray[0], tiArray[1], period, validTimeRange);
        } else {
            return [];
        }
    },

    onAdd: function(map) {
        // Don't call prototype so this_update() does not get called twice
        //L.TimeDimension.Layer.prototype.onAdd.call(this, map);
        this._map = map;
        if (!this._timeDimension && map.timeDimension) {
            this._timeDimension = map.timeDimension;
        }
        this._timeDimension.on("timeloading", this._onNewTimeLoading, this);
        this._timeDimension.on("timeload", this._update, this);
        this._timeDimension.registerSyncedLayer(this);
        map.addLayer(this._baseLayer);
        this._update();
    },

    onRemove: function(map) {
        this._timeDimension.unregisterSyncedLayer(this);
        this._timeDimension.off("timeloading", this._onNewTimeLoading, this);
        this._timeDimension.off("timeload", this._update, this);
        //this._baseLayer.getContainer().style.display = "none";
        Object.keys(allTiles).forEach(layer => { // hide all tiles
            glmap.getMapboxMap().setPaintProperty(layer, "fill-opacity", 0);
        });
        //this.eachLayer(map.removeLayer, map);
        //this._map = null;
    },

    isReady: function(time) {
        // to be implemented for each type of layer
        return true;
    },

    _update: function() {
        if (!this._baseLayer || !this._map) {
            return;
        }

        let time = this._timeDimension.getCurrentTime();

        // get data for time
        let d = new Date(time),
            year = d.getFullYear().toString(),
            m = d.getUTCMonth(),
            month = monthArray[m].toLowerCase(),
            monthYear = `${month}_${year}`;

        // Update title
        let title = $("#title");
        title.html(`<h2>Cobertura de agua en la cuenca del r&iacute;o Grijalva en ${month} de ${year}</h2>`);

        Object.keys(allTiles).forEach(layer => {
            if (layer.split("cuerpos_")[1] !== monthYear) { // hide all other months
                glmap.getMapboxMap().setPaintProperty(layer, "fill-opacity", 0);
            } else { // except current one
                glmap.getMapboxMap().setPaintProperty(layer, "fill-opacity", 0.5);
            }
        });
    }
});

L.timeDimension.layer.Tile = (layer, options) => {
    return new L.TimeDimension.Layer.Tile(layer, options);
};

let legend = L.control({
    position: "bottomright"
});

legend.onAdd = () => {
    let div = L.DomUtil.create("div", "info legend leaflet-bar");
    let html = "<h6>Cuerpos de agua</h6><ul>";
    Object.keys(colors).forEach( c => {
        html += `<li><i style="background: ${colors[c].fill}"></i>${c}</li>`;
    });
    html += "</ul>";
    div.innerHTML = html;
    return div;
};

let radarData = [],
    lineAreaData = [],
    barChartData = [],
    yearList,
    previouslyQueriedMonths = [];

const makeCharts = () => {

    // create sets of unique months and years
    yearList = [... new Set(userDates.map( date => new Date(date).getFullYear() ))];
    let allYears = [... new Set(dateArray.map( date => new Date(date).getFullYear() ))];
    //let formatter = new Intl.DateTimeFormat("es", { month: "long" });

    // RADAR CHART
    // async queries for each date that has been loaded
    const queries = userFiles.map(async (mes, i) => {
        let query = `${baseUrl}/query/${mes}?columns=sum(areacpo)/10000 area`;
        const queryData = await d3.json(query);
        queryData[0].date = new Date(userDates[i]);
        radarData.push(queryData[0]);
        previouslyQueriedMonths.push(userDates[i]);
        return new Promise(resolve => {
            resolve(queryData[0]);
        });
    });
    // wait for all queries to complete and then set chart data
    Promise.all(queries).then( data => {
        radarData.sort((a, b) => (a.date > b.date) ? 1 : -1);
        let radarChartData = [];
        monthArray.forEach( (month, index) => {
            let element = {};
            yearList.forEach( year => {
                element.month = month.toLowerCase();
                let yearData = data.filter(d => new Date(d.date).getMonth() == index && new Date(d.date).getFullYear() == year);
                element[year] = yearData.length > 0 ? yearData[0].area : null;
            });
            radarChartData.push(element);
        });
        makeRadarChart(radarChartData, monthArray, allYears);
    });

    // LINE + STACKED AREA CHARTS
    // async queries for each date that has been loaded
    const queries_ls = userFiles.map(async (mes, i) => {
        let query = `${baseUrl}/query/${mes}?columns=sum(areacpo)/10000 area, descrip&group=descrip`;
        const queryData = await d3.json(query);
        for (const wbody in queryData) {
            queryData[wbody].date = new Date(userDates[i]);
        }
        // create an object {dateValue: queriedData} where each date value has an array of data
        let dataObject = {[userDates[i]]: queryData};
        lineAreaData.push(dataObject);
        return new Promise(resolve => {
            resolve(dataObject);
        });
    });
    // wait for all queries to complete and then set chart data
    Promise.all(queries_ls).then( data => {
        // unnest andd flatten data object for further processing
        let rawData = data.map( d => Object.values(d)[0] ).flat();
        let merged = [].concat.apply([], rawData);
        let aguaPermanente = merged.filter(d => d.descrip == "Agua permanente" );
        let aguaTemporal = merged.filter(d => d.descrip == "Áreas temporalmente inundadas" );
        let aguaVegetacion = merged.filter(d => d.descrip == "Suelos húmedos-vegetación acuática" );

        let chartData = [];
        aguaPermanente.forEach( (wb, i) => {
            let element = {
                permanente: aguaPermanente[i].area,
                temporal: aguaTemporal[i].area,
                vegetacion: aguaVegetacion[i].area,
                date: aguaPermanente[i].date
            }
            chartData.push(element);
        });

        chartData.sort((a, b) => (a.date > b.date) ? 1 : -1);
        makeLineChart(chartData);
        makeAreaChart(chartData);
    });

    // BAR CHART

    // async queries for each date that has been loaded
    const queries_pb = userFiles.map(async (mes, i) => {
        let query_pb = `${baseUrl}/query/${mes}?columns=descrip, areacpo`;
        const queryData = await d3.json(query_pb);
        let date = new Date(userDates[i]);
        for (let k = 0; k < queryData.length; k++) {
            queryData[k].date = date; // assign date to each element
            // assign bar groups to each element
            // TODO: better way to write these conditions?
            if (Math.log(queryData[k].areacpo) <= 1) { queryData[k].grupo = 1 } else if (Math.log(queryData[k].areacpo) <= 2) { queryData[k].grupo = 2 } else if (Math.log(queryData[k].areacpo) <= 3) { queryData[k].grupo = 3 } else if (Math.log(queryData[k].areacpo) <= 4) { queryData[k].grupo = 4 } else if (Math.log(queryData[k].areacpo) <= 5) { queryData[k].grupo = 5 } else if (Math.log(queryData[k].areacpo) <= 6) { queryData[k].grupo = 6 } else if (Math.log(queryData[k].areacpo) <= 7) { queryData[k].grupo = 7 } else if (Math.log(queryData[k].areacpo) <= 8) { queryData[k].grupo = 8 } else if (Math.log(queryData[k].areacpo) <= 9) { queryData[k].grupo = 9 } else if (Math.log(queryData[k].areacpo) <= 10) { queryData[k].grupo = 10 } else if (Math.log(queryData[k].areacpo) <= 11) { queryData[k].grupo = 11 } else if (Math.log(queryData[k].areacpo) <= 12) { queryData[k].grupo = 12 } else if (Math.log(queryData[k].areacpo) <= 13) { queryData[k].grupo = 13 } else if (Math.log(queryData[k].areacpo) <= 14) { queryData[k].grupo = 14 } else if (Math.log(queryData[k].areacpo) <= 15) { queryData[k].grupo = 15 } else if (Math.log(queryData[k].areacpo) <= 16) { queryData[k].grupo = 16 } else if (Math.log(queryData[k].areacpo) <= 17) { queryData[k].grupo = 17 } else if (Math.log(queryData[k].areacpo) <= 18) { queryData[k].grupo = 18 } else if (Math.log(queryData[k].areacpo) <= 19) { queryData[k].grupo = 19 } else if (Math.log(queryData[k].areacpo) <= 20) { queryData[k].grupo = 20 } else { queryData[k].grupo = 20.0301186564 }
            //if (k <= 20 && Math.log(queryData[k].areacpo) <= k + 1) { queryData[k].grupo = k + 1 } else { queryData[k].grupo = 20.0301186564 }
        }
        barChartData.push(queryData);
        return new Promise(resolve => {
            resolve(queryData);
        });
    });

    // wait for all queries to complete and set barChartData to its response
    Promise.all(queries_pb).then((d) => {
        let barData = [].concat.apply([], d);
        // set initial date to first date
        let filterDate = new Date(userDates[0]);
        makeBarChart(barData);
        updateBarChartOnHover(filterDate, barData);
    });

    // end bars
}
const updateBarChartOnHover = (filterDate, data) => {
    let popBarsChart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "barChart");
    let areclass = data.filter(d => d.date.valueOf() == filterDate.valueOf());
    let elementpb = {},
        cart_pb = [];

    // TODO: prepare all these group data sets instead of recalculating when calling the function
    for (let cntr = 0; cntr < 22; cntr++) {
        elementpb = {};
        elementpb.grupo = Math.exp(cntr) / 10000;
        elementpb.ap = (areclass.filter(function(d) { return d.descrip == "Agua permanente" && d.grupo == cntr })).length;
        elementpb.at = (areclass.filter(function(d) { return d.descrip == "Áreas temporalmente inundadas" && d.grupo == cntr })).length;
        elementpb.av = (areclass.filter(function(d) { return d.descrip == "Suelos húmedos-vegetación acuática" && d.grupo == cntr })).length;
        cart_pb.push(elementpb);
    }

    let cart_pb1 = cart_pb.filter(function(d) { return d.ap > 0 || d.at > 0 || d.av > 0 });

    cart_pb1.sort((a, b) => (a < b ? 1 : -1));
    if (popBarsChart) {
        popBarsChart.data = cart_pb1;
        popBarsChart.invalidateRawData();
        // set barChart title when hovering on different bars
        let ye = new Intl.DateTimeFormat("es", { year: "numeric" }).format(filterDate);
        let mo = new Intl.DateTimeFormat("es", { month: "short" }).format(filterDate);
        popBarsChart.titles.values[0].text = `Conteo por tipo de cuerpo en ${mo} ${ye}`;
        // TODO: update barChart when changing dates on timeDimension?
    }
}

const updateCharts = () => {
    yearList = [... new Set(userDates.map( date => new Date(date).getFullYear() ))];

    // update radar chart
    let radarChart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "radarChart");

    const queries = userDates.map( async (date, i) => {
        // check what month has not been queried before and query it
        if (!radarData.some(d => d.date.valueOf() === date)) {
            let query = `${baseUrl}/query/${userFiles[i]}?columns=sum(areacpo)/10000 area`;
            const queryData = await d3.json(query);
            queryData[0].date = new Date(date);
            radarData.push(queryData[0]);
            //previouslyQueriedMonths.push(date);
            return new Promise(resolve => {
                resolve(queryData[0]);
            });
        }
    });

    // wait for all queries to complete and then update chart data
    Promise.all(queries).then( () => {
        let radarChartData = [];
        // check what months should have data
        //radarData.sort((a, b) => (a.date > b.date) ? 1 : -1);
        /*let monthCheck = radarData.map( (d,i) => d.date.valueOf() == userDates[i]);
        let newRadarData = radarData.map( (d,i) => {
            if (monthCheck[i]) return d;
        });
        let filtredRadarData = newRadarData.filter( d => d != undefined );*/
        let filtredRadarData = radarData.filter( d => userDates.includes(d.date.valueOf()));
        monthArray.forEach( (month, index) => {
            let element = {};
            yearList.forEach( year => {
                element.month = month.toLowerCase();
                let yearData = filtredRadarData.filter(d => new Date(d.date).getMonth() == index && new Date(d.date).getFullYear() == year);
                element[year] = yearData.length > 0 ? yearData[0].area : null;
            });
            radarChartData.push(element);
        });
        radarChart.data = radarChartData;
        radarChart.invalidateRawData();
    });

    // update line + area chart
    let lineChart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "lineChart"),
        areaChart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "areaChart");

    // extract months as values for which there is data already queried
    let chartMonths = lineAreaData.map( d => parseInt(Object.keys(d)[0]) );
    const queries_ls = userDates.map( async (date, i) => {
        // check what month has not been queried before and query it
        if (!chartMonths.some( d => d === date)) {
            let query = `${baseUrl}/query/${userFiles[i]}?columns=sum(areacpo)/10000 area, descrip&group=descrip`;
            const queryData = await d3.json(query);
            for (const wbody in queryData) {
                queryData[wbody].date = new Date(userDates[i]);
            }
            // create an object {dateValue: queriedData} where each date value has an array of data
            let dataObject = {[userDates[i]]: queryData};
            lineAreaData.push(dataObject);
            return new Promise(resolve => {
                resolve(queryData);
            });
        }
    });

    // wait for all queries to complete and then update chart data
    Promise.all(queries_ls).then( () => {

        // unnest and flatten data object and filter those for the appropriate dates
        let rawData = lineAreaData.map( d => Object.values(d)[0] ).flat()
            .filter( d => userDates.includes(d.date.valueOf()));

        let merged = [].concat.apply([], rawData);
        let aguaPermanente = merged.filter(d => d.descrip == "Agua permanente" );
        let aguaTemporal = merged.filter(d => d.descrip == "Áreas temporalmente inundadas" );
        let aguaVegetacion = merged.filter(d => d.descrip == "Suelos húmedos-vegetación acuática" );

        let chartData = [];
        aguaPermanente.forEach( (wb, i) => {
            let element = {
                permanente: aguaPermanente[i].area,
                temporal: aguaTemporal[i].area,
                vegetacion: aguaVegetacion[i].area,
                date: aguaPermanente[i].date
            }
            chartData.push(element);
        });

        // update charts ranges
        let lineDateAxis = lineChart.xAxes.values[0];
        lineDateAxis.axisRanges.clear();
        makeDateRange(lineDateAxis, 0, 2, "#E1DBC8");
        makeDateRange(lineDateAxis, 2, 2, "#595952");
        makeDateRange(lineDateAxis, 4, 4, "#838278");
        makeDateRange(lineDateAxis, 8, 3, "#B1AE9F");
        makeDateRange(lineDateAxis, 11, 1, "#E1DBC8");

        let areaDateAxis = areaChart.xAxes.values[0];
        areaDateAxis.axisRanges.clear();
        makeDateRange(areaDateAxis, 0, 2, "#E1DBC8");
        makeDateRange(areaDateAxis, 2, 2, "#595952");
        makeDateRange(areaDateAxis, 4, 4, "#838278");
        makeDateRange(areaDateAxis, 8, 3, "#B1AE9F");
        makeDateRange(areaDateAxis, 11, 1, "#E1DBC8");

        // updte chart data
        chartData.sort((a, b) => (a.date > b.date) ? 1 : -1);
        lineChart.data = chartData;
        lineChart.invalidateRawData();
        areaChart.data = chartData;
        areaChart.invalidateRawData();
    });

    // update bar chart

    let barChart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "barChart");
    previouslyQueriedMonths.sort((a, b) => (a > b) ? 1 : -1);
    // TODO: how to use this previouslyQueriedMonths varr for all charts?
    // If it is set in one chart, it may affect all others.
    // maybe use an object with keys for each chart, e.g., [{radar: [months], lineArea: [months], bars: [months]}]
    // abnd update previouslyQueriedMonths.radar.push() or previouslyQueriedMonths.lineArea.push() etc.

    // async queries for each date that has been loaded
    const queries_pb = userDates.map(async (date, i) => {
        if (!previouslyQueriedMonths.some(d => d == date)) {
            let query_pb = `${baseUrl}/query/${userFiles[i]}?columns=descrip, areacpo`;
            const queryData = await d3.json(query_pb);
            let dateValue = new Date(userDates[i]);
            for (let k = 0; k < queryData.length; k++) {
                queryData[k].date = dateValue; // assign date to each element
                // assign bar groups to each element
                // TODO: better way to write these conditions?
                if (Math.log(queryData[k].areacpo) <= 1) { queryData[k].grupo = 1 } else if (Math.log(queryData[k].areacpo) <= 2) { queryData[k].grupo = 2 } else if (Math.log(queryData[k].areacpo) <= 3) { queryData[k].grupo = 3 } else if (Math.log(queryData[k].areacpo) <= 4) { queryData[k].grupo = 4 } else if (Math.log(queryData[k].areacpo) <= 5) { queryData[k].grupo = 5 } else if (Math.log(queryData[k].areacpo) <= 6) { queryData[k].grupo = 6 } else if (Math.log(queryData[k].areacpo) <= 7) { queryData[k].grupo = 7 } else if (Math.log(queryData[k].areacpo) <= 8) { queryData[k].grupo = 8 } else if (Math.log(queryData[k].areacpo) <= 9) { queryData[k].grupo = 9 } else if (Math.log(queryData[k].areacpo) <= 10) { queryData[k].grupo = 10 } else if (Math.log(queryData[k].areacpo) <= 11) { queryData[k].grupo = 11 } else if (Math.log(queryData[k].areacpo) <= 12) { queryData[k].grupo = 12 } else if (Math.log(queryData[k].areacpo) <= 13) { queryData[k].grupo = 13 } else if (Math.log(queryData[k].areacpo) <= 14) { queryData[k].grupo = 14 } else if (Math.log(queryData[k].areacpo) <= 15) { queryData[k].grupo = 15 } else if (Math.log(queryData[k].areacpo) <= 16) { queryData[k].grupo = 16 } else if (Math.log(queryData[k].areacpo) <= 17) { queryData[k].grupo = 17 } else if (Math.log(queryData[k].areacpo) <= 18) { queryData[k].grupo = 18 } else if (Math.log(queryData[k].areacpo) <= 19) { queryData[k].grupo = 19 } else if (Math.log(queryData[k].areacpo) <= 20) { queryData[k].grupo = 20 } else { queryData[k].grupo = 20.0301186564 }
                //if (k <= 20 && Math.log(queryData[k].areacpo) <= k + 1) { queryData[k].grupo = k + 1 } else { queryData[k].grupo = 20.0301186564 }
            }
            previouslyQueriedMonths.push(date);
            barChartData.push(queryData);
            return new Promise(resolve => {
                resolve(queryData);
            });
        }
    });

    // wait for all queries to complete and use barChartData to update chart
    Promise.all(queries_pb).then(() => {
        let barData = [].concat.apply([], barChartData);
        // set initial date to first date
        let filterDate = new Date(userDates[0]);
        barChart.data = barData;
        barChart.invalidateRawData();
        updateBarChartOnHover(filterDate, barData);
    });

}