/*
 * Copyright 2019 - All rights reserved.
 * Rodrigo Tapia-McClung
 *
 * August 2019
 */

/* global Promise, chroma */

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,
    glmap,
    timeLayer;

// define empty objects and indicators
let maxIndicators = {},
    minIndicators = {},
    indicators = ["area", "perimeter", "costa", "df"],
    indicatorsNames = ["Área", "Perímetro", "Desarrollo de la línea de costa", "Dimensión fractal"];

let cols = [];
indicators.forEach( (indicator) => {
    cols.push("min(" + indicator + ") as min" + indicator);
    cols.push("max(" + indicator + ") as max" + indicator);
});

let currentTiles = {},
    allTiles = {};

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

    d3.timeFormatDefaultLocale(locale);

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

    setupTimeDimensionControl();
    setupDates()
        .then(dates => populateDates(dates))
        .then(userData => setupMap(userData))
        .then(map => populateMap(map));
});

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

// query available dates on DB
const setupDates = () => {
    return new Promise( resolve => {
        let layersQuery = "http://localhost:8090/data/list_layers";
        d3.json(layersQuery).then(layers => {
            layers.forEach(layer => {
                dateArray.push(timeParse(layer.f_table_name)); // convert filenames to dates
            })

            dateArray = dateArray.sort(sortInitialDateAscending); // order dates
            dateMin = d3.min(dateArray);
            dateMax = d3.max(dateArray);
            //userFiles = dateArray.map( month => timeFormat(month)); // order table names by date

            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

                // TODO: on close trigger update map
                if (!map) {
                    resolve({min: startUserDate, max: endUserDate});
                } else {
                    // FIXME: no need to pass data here?
                    updateMap({map: map, min: startUserDate, max: endUserDate});
                }
            },
            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 => {
        map = L.map("mapmex", {
            center: [17.22, -92.28],
            minZoom: 7,
            zoom: 7,
            timeDimension: true,
            timeDimensionOptions: {
                times: userDates,
                currentTime: dates.min
            }
        });

        let 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."
        });

        cartoDarkLayer.addTo(map);

        // initialize min and max objects to hold values for each indicator
        // and add select options
        indicators.forEach( (indicator, index) => {
            maxIndicators[indicator] = 0;
            minIndicators[indicator] = 1e30;
            $("#indicatorSelect").append("<option value=\""+ indicator + "\">" + indicatorsNames[index] + "</option>");
        });

        console.log(userFiles);
        // query db to get min/max values per month and indicator and store them in an object
        queryFiles().then( minmax => {
            minmax.map( minmaxMonth => {
                indicators.forEach( (indicator) => {
                    minIndicators[indicator] = Math.min(minIndicators[indicator], minmaxMonth["min" + indicator]);
                    maxIndicators[indicator] = Math.max(maxIndicators[indicator], minmaxMonth["max" + indicator]);
                });
            });
            timeDimensionControl.addTo(map);
            resolve({"map": map, "minIndicators": minIndicators, "maxIndicators": maxIndicators});
        });
    });
}

const flatten = arrays => {
    return [].concat(arrays);
}

const queryFiles = () => {
    return Promise.all(userFiles.map(getMinMax))
        // the result is an array of arrays, so we'll flatten them here
        .then(flatten);
}

const getMinMax = table => {
    return new Promise( resolve => {
        let minmaxQuery = "http://localhost:8090/data/query/" + table + "?columns=" + cols.join(", ");
        d3.json(minmaxQuery).then( minmax => {
            resolve(minmax[0]);
        });
    });
}

const updateMap = (mapData) => {
    //console.log(mapData);
    console.log(userFiles);

    // ckear 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
            //currentJSONs[monthYear] = allJSONs[monthYear]; // recover json 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._glMap.addLayer(newTile);
            if (monthYear == userFiles[0]) {
                glmap._glMap.setPaintProperty(monthYear, 'fill-opacity', 0.7)
            }
        }
    });

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

    // clear minmax indicators objects
    maxIndicators = {},
    minIndicators = {};
    indicators.forEach( (indicator) => {
        maxIndicators[indicator] = 0;
        minIndicators[indicator] = 1e30;
    });

    // query db for new minmax values then style tiles
    new Promise( resolve => {
        queryFiles().then( minmax => {
            minmax.map( minmaxMonth => {
                indicators.forEach( (indicator) => {
                    minIndicators[indicator] = Math.min(minIndicators[indicator], minmaxMonth["min" + indicator]);
                    maxIndicators[indicator] = Math.max(maxIndicators[indicator], minmaxMonth["max" + indicator]);
                });
            });
            resolve({"map": map, "minIndicators": minIndicators, "maxIndicators": maxIndicators});
        })
    }).then( values => { // once we have new minmax values, style all tiles
        let option = $("#indicatorSelect").val(), // option selected from dropdrown
            min = values.minIndicators,
            max = values.maxIndicators;
        styleTiles(option, min, max);
    });
}

const populateMap = (mapData) => {
    let map = mapData.map,
        minIndicators = mapData.minIndicators,
        maxIndicators = mapData.maxIndicators;

    // create mvt layers
    userFiles.forEach( f => {
        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._glMap.on("style.load", () => {
        userFiles.forEach(monthYear => {
            glmap._glMap.addLayer(currentTiles[monthYear]);
        });

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

        // 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);

        // style currentTiles
        let option = $("#indicatorSelect").val(); // option selected from dropdrown
        styleTiles(option, minIndicators, maxIndicators);
    });
}

// define MVT layer for given month table and all indicators
const mapboxLayer = (monthYear) => {
    let pbfLayer = {
        id: monthYear,
        source: {
            type: 'vector',
            tiles: ["http://localhost:8090/data/" + monthYear + "/mvt/{z}/{x}/{y}?geom_column=geom&columns=" + indicators.join()],
            maxzoom: 14,
            minzoom: 5
        },
        'source-layer': monthYear,
        type: 'fill',
        minzoom: 5,
        'paint': {
            'fill-opacity': 0,
            /*'fill-color': [
                'interpolate',
                ['linear'],
                ['get', 'df'],
                1, 'rgba(255, 0, 0, 0.5)', // red
                1.3, 'rgba(0, 255, 0, 0.5)', // green
            ]/*,
            'fill-outline-color': [
                'interpolate',
                ['linear'],
                ['get', 'df'],
                1, 'rgba(255, 0, 0, 0.6)', // red
                1.3, 'rgba(0, 255, 0, 0.6)', // green
            ]*/
        }
    }
    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
        //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);
        // Remove base layer caue mapbox layer is not an L layer...
        map.addLayer(this._baseLayer);
        // Don't update on add. Rather check @708 and what happens there
        //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";
        //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;
        }

        var 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>");

        // Update graphs only on timeload event
        //indicators.forEach( indicator => {
        //    indicatorVars[indicator].chartData = indicatorVars[indicator].chart.data(); // get chart data
        //    indicatorVars[indicator].chart.data(indicatorVars[indicator].chartData); // set chart data
        //});

        //console.time("process");
        console.log("data for", monthYear);
        //console.log(currentTiles)
        Object.keys(allTiles).forEach(layer => {
            if (layer !== monthYear) { // hide all other months
                glmap._glMap.setPaintProperty(layer, 'fill-opacity', 0);
            } else { // except current one
                glmap._glMap.setPaintProperty(layer, 'fill-opacity', 0.7);
            }
        });
        //console.timeEnd("process");
    }
});

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


// When selecting indicator from dropdown, style tiles.
$("#indicatorSelect").on("change", function() {
    // style currentTiles
    let option = this.value; // option selected from dropdrown
    styleTiles(option, minIndicators, maxIndicators);
    //    .then(legend.addTo(map)); // add legend control -> it updates
    // FIXME: re-adding control updates its contents... why?
    // Highlight plot title according to selected option
    //indicators.forEach( indicator => {
    //    d3.select(indicatorVars[indicator].container).select("svg text.title").classed("active", indicator === option ? true : false);
    //});
});

const styleTiles = (option, minIndicators, maxIndicators) => {
    // define color scale domain based on min-max values for selected indicator
    let domain = [minIndicators[option], maxIndicators[option]];
    //console.log(domain)
    let scale = chroma.scale("PuBu").padding([0.5, 0]).domain(domain).classes(5);
    Object.keys(currentTiles).forEach(layer => {
        let color = [
            'interpolate',
            ['linear'],
            ['get', option],
            minIndicators[option], scale(minIndicators[option]).hex(),
            maxIndicators[option], scale(maxIndicators[option]).hex()
        ];
        glmap._glMap.setPaintProperty(layer, 'fill-color', color);
    });
    return Promise.resolve(scale);
}

// TODO: add layer control
// TODO: add charts
