//  Graph

/*
interface:

updateCharts2(graph)
   graph is a dict <key> iof array of (t,y)

   called when data is updated

graphs.receivedVars(blocks)

   block is a list of dict(tag, unit, curves)
   curves is a dict(name, label, color, period)


internals:

graphs: containing main objects and methods
   with the arrays:
       graph_array: array fo graphs
       graph_elm_array: the array of elements containing the graphs
       vars_array: the array containing the variables meta data

graph = Graph(..): one graph with meta data and methods for one chart (short: ch)
in loops called gr

chart = Chart(...): the chartjs chart
*/

// Unused ?
function Timer(){
    let start = window.performance.now();
    return function(x = "timer"){
        console.log(x, window.performance.now()-start);
    }
}

// Sets the graph background at loading
window.addEventListener('load', function(){
    var urlParams = new URLSearchParams(window.location.search);
    if(urlParams.has('white')){
        document.body.classList.add('white');
    }
    else if(urlParams.has('black')){
        document.body.classList.add('black');
    }
})

/*
function addClass(obj, cls){
    if(!obj.classList.contains(cls))
        obj.classList.add(cls)
}

function delClass(obj, cls){
    if(obj.classList.contains(cls))
        obj.classList.remove(cls)
}
*/

/**
 * Function used to make AJAX request, with three nested functions (POST, GET with returned JSON data, GET)
 * @param {string} addr - The endpoint
 * @returns This
 */
function AJAX(addr){
    var xhr = new XMLHttpRequest();

    if (debugCommunication) console.log('AJAX', addr);
    this.sendJSON = function(array, returnIsJSON = true){
        xhr.open("POST", addr, true);
        xhr.send(JSON.stringify(array));
        return new Promise(function(resolve, reject){
            xhr.addEventListener('load', function(){
                if(this.status == 200){
                    if(returnIsJSON){
                        this.responseJSON = JSON.parse(this.responseText);
                    }
                    resolve(this);
                }
            });
        });
    }

    this.getJSON = function(){
        xhr.open("GET", addr, true);
        xhr.send();
        return new Promise(function(resolve, reject){
            xhr.addEventListener('load', function(){
                if(this.status == 200){
                    if (debugCommunication) console.log('A RES', JSON.parse(this.responseText));
                    resolve(JSON.parse(this.responseText));
                }
            });
        });
    }

    // arguments are passed like param1=value1&param2=value2...
    this.postForm = function(args){
        xhr.open("POST", addr, true);
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xhr.send(args);
        return new Promise(function(resolve, reject){
            xhr.addEventListener('load', function(){
                if(this.status == 200){
                    if (debugCommunication) console.log('A RES', JSON.parse(this.responseText));
                    resolve(JSON.parse(this.responseText));
                }
            });
        });
    }

    this.get = function(responseType = "text"){
        xhr.open("GET", addr, true);
        xhr.responseType = responseType;
        xhr.send();
        return new Promise(function(resolve, reject){
            xhr.addEventListener('load', function(){
                if(this.status == 200){
                    resolve(this);
                }
            });
        });
    }

    return this;


}

/**
 * Performs a callback on double tap (defined as two taps less than 500ms and in a window of +-40px in x and y directions)
 * 
 * Timeout things maight be useless...
 * 
 * @param {*} callback - The callback to execute
 * @returns - Never used
 */
function doubleTap(callback){
    var timeout;
    var lastTap = 0, lastX=NaN, lastY=NaN;
    function handler(event) {
        console.log(event);
        var currentTime = new Date().getTime();
        var tapLength = currentTime - lastTap;
        let touch = event.changedTouches ? event.changedTouches[0] : event,
        x = touch.clientX,
        y = touch.clientY;
        clearTimeout(timeout);
        if (tapLength < 500 && tapLength > 0 && Math.abs(lastX-x) < 40 && Math.abs(lastY-y) < 40) {
            event.preventDefault();
            callback()
        } else {
            timeout = setTimeout(function() {
                clearTimeout(timeout);
            }, 500);
        }
        lastTap = currentTime;

        lastX = x;
        lastY = y;
    }
    window.addEventListener('touchend', handler);
    return {stop: function(){ window.removeEventListener('touchend', handler) }}
}
/**
 * Return the highest value out of a time interval in a set of points
 * @param {*} array - The array we want to look the highest value for
 * @param {*} tmin - The lower bound of the time interval
 * @param {*} tmax - The upper boud of the time interval
 * @returns The highest found value (y value)
 */
function maxAr(array, tmin, tmax){
    return Math.max.apply(Math, array.map(function(o) {
        if (o.y == null || o.x < tmin || o.x > tmax) return -1e99;
        return o.y;
    }));
}

/**
 * Return the lowest value out of a time interval in a set of points
 * @param {*} array - The array we want to look the lowest value for
 * @param {*} tmin - The lower bound of the time interval
 * @param {*} tmax - The upper boud of the time interval
 * @returns The lowest found value (y value)
 */
function minAr(array, tmin, tmax){
    return Math.min.apply(Math, array.map(function(o) {
        if (o.y == null || o.x < tmin || o.x > tmax) return 1e99;
        return o.y;
    }));
}

/**
 * Formats the values to display (especially the exponentials)
 * @param {*} val - The value to format
 * @param {number} significant_digits - The max number of digits to display
 * @returns The formatted value as a string
 */
function strFormat(val, significant_digits=13) {
    if (val == null) return '';
    evalue = val.toExponential(significant_digits-1).replace(/0*e/, 'e').replace(/\.e/, 'e').replace("e+", "e");
    fvalue = Number.parseFloat(evalue).toString();
    if (fvalue.length <= evalue.length)
        return fvalue;
    else
        return evalue;
}

// Defining keys for global controls

let xyKey = "xy-control";

/**
 * Function used to handle some global controls for the graphs
 */
let globalControls = (function (){
    let controlsMap = {};

    function loadControls(panel){
        let controlBar = document.createElement("div");
        controlBar.id = "control_bar";
        panel.appendChild(controlBar);

        let xyControl = new Control("res/x_zoom_white_wide.png", "res/y_zoom_white_wide.png", "Time<->Y zoom (one graph)", graphs.toggleZoomMode, graphs.toggleZoomMode);
        
        controlBar.appendChild(xyControl);

        controlsMap[xyKey] = xyControl;
    }

    function getControlsMap(){
        return controlsMap;
    }

    return {
        loadControls: loadControls,
        getControlsMap: getControlsMap,
    }
})();

let datesPopup = undefined;

/**
 * Used to add the Dates Popup (initialization + adding it to the DOM)
 */
function loadDatesPopup(){
    let graphsContainer = document.getElementsByClassName("graphs-container")[0];
    datesPopup = new DatesPopup(graphs.gotoNow, graphs.jumpToDate);
    graphsContainer.appendChild(datesPopup);
}

// Defining keys for global indicators

let datesKey = "dates-indicator";

/**
 * Function used to handle some global indicators for the graphs
 */
let globalIndicators = (function (){

    let indicatorsMap = {}

    function loadIndicators(panel){
        let leftDate = Date.now() - 30*60*1000;
        let datesIndicator = new DateIndicator(leftDate);
        datesIndicator.addEventListener("click", function () {
            exportPopup.hide();
            curvesSettingsPopup.hide();
            menuGraphicsPopup.hide();
            datesPopup.show();
        })
        
        panel.appendChild(datesIndicator);
        datesIndicator.style.marginLeft = "auto";
        datesIndicator.style.marginRight = "auto";

        indicatorsMap[datesKey] = datesIndicator;
    }

    function getIndicatorsMap(){
        return indicatorsMap;
    }

    return {
        loadIndicators: loadIndicators,
        getIndicatorsMap: getIndicatorsMap
    }

})()

let menuGraphicsPopup = undefined;

/**
 * Defines all the entries in the menu and adds it to the DOM
 * @param {*} panel The panel on which to add the menu icon
 */
function loadGraphicsMenu(panel){

    menuGraphicsPopup = new MenuPopup();
    let exportActionEntry = new ActionEntry("Export", graphs.displayExportPopup, () => {menuGraphicsPopup.hide()});
    let curvesSettingsActionEntry = new ActionEntry("Curves settings", () => {curvesSettingsPopup.show();}, () => {menuGraphicsPopup.hide()});

    let hideShowInstrumentDevicesHelpEntry = new HelpEntry("Hide/show instrument and devices", "If the 'instrument and devices' label is taking too much space, you can click on it (not on the house at the left) to hide it. " +
        "You can then click once again on the left gray square to make it appear again."
    );
     let toggleZoomModeHelpEntry = new HelpEntry("Toggle the zoom mode", "On the top curves bar, a horizontal double arrow means that a zoom gesture will trigger a zoom on the time axis of all the graphs. "
        + "Clicking on it will change the zoom mode to the y direction. If you perform a zoom gesture on a graph, it will zoom in the y direction while disabling the autoscale for this graph. "
        + "You can go back to the time zoom mode by clicking again on the arrow."
    )
    let hideShowCursorHelpEntry = new HelpEntry("Hide/show the cursor", "To show the cursor, you can click on any graph. To hide it, you can double click/tap on any graph.");
    let hideShowLegendsHelpEntry = new HelpEntry("Hide/showe the legends", "You can remove all legends by clicking on the cross of one of them. They will appear again if you click on any graph.")
    
    let showMoreCurvesHelpEntry = new HelpEntry("Show more curves", "On each graph, you can click on the unit at the top right hand corner to open a selection window. There, you can choose which block you want to display. " + 
        "You can also add curves (especially parameters) with the curves settings."
    );
    let historicalDataHelpEntry = new HelpEntry("Look for historical data", "Clicking on the date displayed at the top of the curves opens a popup."+
        "You can then select the date you want to jump to, and the time at which you want the available curves. You can also go to now. The date displayed will be the one corresponding "
        + "to the lower bound of the viewing window. On wide screens, a jump will close the right part, and going to now will display it back.");
   
    let aboutCurvesSettingsHelpEntry = new HelpEntry("About curves settings", "To indicate a variable, you need to type the SECOP module name (case sensitive) of the variable you want to act on. " +
        "Leaving the parameter empty means that you will act on the value parameter (but you can still write 'value'). For the category, '-' means that this curve will be hidden. To display a curve which is not displayed yet, the category has to be filled. "
    + "The character '*' means that the curve will be displayed in the block of its unit. If one of the fields are filled (for the category, other than '*') but not the variable, an error will be thrown. "
    + "If nothing is set for the category (except '*'), the color and the unit, the row will be ignored. Closing the popup via the cross or the 'Apply' button will save your configuration and reload the curves. "
    + "You can discard your changes via the 'Cancel' button.")

    let aboutTopRightHandCornerCrossHelpEntry = new HelpEntry("About top right hand corner cross", "On wide screens, this cross toggles the right part visibility when control is enabled for this instrument. "
        + "On small screens (mobile phones for example), it allows you to switch to the next section of the app (from graphs block to main block to console block)."
    );
    menuGraphicsPopup.addEntry(exportActionEntry)
    menuGraphicsPopup.addEntry(curvesSettingsActionEntry);
    menuGraphicsPopup.addHorizontalDivider();
    menuGraphicsPopup.addEntry(hideShowInstrumentDevicesHelpEntry);
    menuGraphicsPopup.addEntry(toggleZoomModeHelpEntry);
    menuGraphicsPopup.addEntry(hideShowCursorHelpEntry);
    menuGraphicsPopup.addEntry(hideShowLegendsHelpEntry);
    menuGraphicsPopup.addEntry(showMoreCurvesHelpEntry);
    menuGraphicsPopup.addEntry(historicalDataHelpEntry);
    menuGraphicsPopup.addEntry(aboutCurvesSettingsHelpEntry);
    menuGraphicsPopup.addEntry(aboutTopRightHandCornerCrossHelpEntry);

    let graphicsMenuControl = new Control("res/menu_white_wide.png", "res/menu_white_wide.png", "Menu", () => {
        datesPopup.hide();
        exportPopup.hide();
        curvesSettingsPopup.hide();
        menuGraphicsPopup.show();
    });
    panel.appendChild(menuGraphicsPopup);
    menuGraphicsPopup.getContainer().style.top = "28px";
    menuGraphicsPopup.getContainer().style.right = "20px";
    panel.appendChild(graphicsMenuControl);
    graphicsMenuControl.style.marginLeft="6px";
    graphicsMenuControl.style.marginRight="22px";
    graphicsMenuControl.style.marginTop="2px";
}

let exportPopup = undefined;

/**
 * Used to add the export Popup (initialization + adding it to the DOM)
 */
function loadExportPopup(){
    let graphsContainer = document.getElementsByClassName("graphs-container")[0];
    exportPopup = new ExportPopup(exportCallback);
    graphsContainer.appendChild(exportPopup);
}

/**
 * Function call when the Export button is clicked in the Export popup
 */
function exportCallback(selectedVariables, startDateTimeMs, endDateTimeMs, nan, binning=null){

    let binningParam = "None";
    if (binning !== null)
        binningParam = binning
    let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binningParam + "&id=" + clientID
    let a = document.createElement('a');
    a.href = exportURL
    a.download = true
    a.click()
}

let curvesSettingsPopup = undefined;

/**
 * Used to add the curves settings Popup (initialization + adding it to the DOM)
 */
function loadCurvesSettingsPopup(){
    let graphsContainer = document.getElementsByClassName("graphs-container")[0];
    curvesSettingsPopup = new CurvesSettingsPopup(graphs.applySettingsCallback);
    graphsContainer.appendChild(curvesSettingsPopup);
}

let graphs = (function (){
    let dataset_to_graph_map = {}; // a dictionnary mapping a variable name to a two values array, containing its graph index and its position inside the graph
    let blocks, liveMode=true, top_vars=[], bottom_vars=[];
    let legendFlag = false;
    let currentZoomMode = isTouchDevice ? 'xy' : 'x';
    let prevTime = null, prevMin = null, prevMax = null, prevGraph = null; // zoom speed limitation
    let cursorLinePos = null; // the position of the cursor line (given by its x value)
    let clickMode = 0;  // 1: mouse is down, 2: pan is active, 0: after mouse down

    let type = 'linear'; // type of graphs axis to display

    let ngraphs = 0; // current number of graphs

    let graph_array = []; // an array of Graph objects
    let graph_elm_array = []; // an array of HTML divs (with appropriate classes) containing the corresponding Graph objects of graph_array
    let vars_array = []; // an array of arrays of curve names, each curve names array is positionned at its graph id

    let tag_dict = {}; // a dictionnary of graph indexes (corresponding to the three indexes of the above arrays), indexed by the tag of the graphs

    let currentMinTime = 0, currentMaxTime = 0; // the currently displayed time range
    let startTime;  // time of query on server
    let recvTime;  // local relative time at receive of above
    let minTime, maxTime; // the queried time range
    let lastTime = 0; // time of most recent data point

    let resolution = undefined; // current window resolution (ms/pixel)

    let activateUpdateTimeout = undefined; // timeout for the activateUpdates function
    let updateAutoTimeout = undefined; // timeout for the updateAuto function (used in onZoomCompleteCallback)

    let container = document.createElement('div');
    container.classList.add("graphs-container");

    /** The current time corrected for server time */
    function now(){
        return startTime + (performance.now()-recvTime);
    }

    /** 
     * Clears a graph (meaning it deletes the datasets for the corresponding Graph object, set the Graph Object to undefined and empties the HTML container) at the given index
     * @param {number} gindex - The graph index to clear
    */
    function clear(gindex){
        let graph_elm = graph_elm_array[gindex];
        let graph = graph_array[gindex];

        graph_elm.innerHTML = '';
        graph_array[gindex] = undefined;
        vars_array[gindex] = [];
        if (graph) {
            for (let key in dataset_to_graph_map) {
                if (dataset_to_graph_map[key][0] == gindex) {
                    delete dataset_to_graph_map[key];
                }
            }
        }
    }


    /**
     * Creates the "unit" selection block in the given gindex div graph container
     * @param {number} gindex - The id of the graph container element where to display the selection block
     */
    function createSelection(gindex){
        let graph_elm = graph_elm_array[gindex];
        clear(gindex);

        for(let tag in tag_dict){
            if (tag_dict[tag] === gindex){ // we indicate that the tag is not displayed
                tag_dict[tag] = -1
                break
            }
        }
        let selection = document.createElement('div');
        selection.classList.add('selection');

        for (let block of blocks) {

            let bel = document.createElement('div');
            bel.classList.add('select');
            let title = document.createElement('div');
            title.classList.add('title');
            title.innerHTML = block.tag;
            bel.appendChild(title);
            let params = document.createElement('div');
            params.classList.add('params');
            for(let param of block.curves){
                let pel = document.createElement('div');
                pel.classList.add('param');
                pel.innerHTML = param.label;
                params.appendChild(pel);
            }
            bel.appendChild(params);
            bel.addEventListener('click', function(){
                if (block.tag in tag_dict) { //if the block that we might select is in the ones that are displayed
                    let idx = tag_dict[block.tag]; // we get the current gindex (idx) of the clicked block                       
                    if(idx != -1){ //if the clicked block is displayed somewhere, we create a selection window
                        createSelection(idx); // We will create a selection at the gindex
                    }
                }                       
                createGraph(gindex, block); // we create at the current shown selector (gindex), the graph corresponding to the one clicked (block)
            })
            selection.appendChild(bel);

        }
        graph_elm.appendChild(selection);
    }

    /**
     * Sets live mode and enable/disable 'go to now' button
     * @param {boolean} mode - Tells if we are in live mode or not
     */
    function setLiveMode(mode=null) {
        if (mode !== null){
            liveMode = mode;
        }
        if (liveMode && cursorLinePos === null)
            // gotoNowElm.innerHTML = '';
            // globalControls.getControlsMap()[goToNowKey].changeToAlt();
            console.log("Need to change to nothing");
        else
            // gotoNowElm.innerHTML = 'go to now';
            // globalControls.getControlsMap()[goToNowKey].changeToMain();
            console.log("Need to change to seen");
    }

    /**
     * Calls the /graph route with the variables of the block
     * Adds the received dataset in the graph being created
     * Sets the visualization window, conditionnaly autoscale, applies the changes, and show the legend
     * @param {number} gindex - The position of the graph in the container
     * @param {{tag:string, unit:string, curves:[{name:string, label:string, color:string}]}} block - The information of the block to create
     */
    function createGraph(gindex, block){
        console.log("clear for create graph", gindex)
        clear(gindex);
        tag_dict[block.tag] = gindex;
        let dict = {} // {string: [name:string, label:string, color:string]}
        for (let curve of block.curves) {
            if (curve.show !== false) {
                vars_array[gindex].push(curve.name);
                dict[curve.name] = curve;
            }
        }
        //let varlist = top_vars.concat(bottom_vars);
        varlist = vars_array[gindex];
        let graph_elm = graph_elm_array[gindex];

        timeDeltaAxis = maxTime - minTime
        setResolution(timeDeltaAxis)

        AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000 + "&variables=" + varlist + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){

            //console.log('Graph', block, data)
            let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type);
            graph_array[gindex] = graph;

            for(let key in data.graph){
                if(!vars_array[gindex].includes(key)){
                    continue;
                }
                let pdata = [];
                for(let e of data.graph[key]){
                    pdata.push({x: e[0]*1000, y: e[1]});
                }
                
                addDataset(gindex, key, pdata, dict[key])
                // if(pdata.length > 0){
                //     addDataset(gindex, key, pdata, dict[key])
                //     /*console.log(timeRange);
                //     if(data[data.length-1].x-data[0].x > d && data[data.length-1].x-data[0].x < (30*60+10)*1000){ // Adjust to requested time
                //         d = data[data.length-1].x-data[0].x
                //         max = data[data.length-1].x;
                //         min = data[0].x;
                //     }*/
                // }
            }
            graph.setMinMax(currentMinTime,currentMaxTime);
            graph.autoScaleIf();
            graph.update();
            showLegends(legendFlag, false);
            if (legendFlag) adjustLegends();

            if (activateUpdateTimeout !== undefined){
                clearTimeout(activateUpdateTimeout)
            }

            activateUpdateTimeout = setTimeout(function(){
                activateUpdates();
            }, 1000);
        })
    }

    /**
     * Adds dataset to the Graph object at gindex position
     * @param {number} gindex - The index of the Graph object to add dataset to
     * @param {string} key - The curve name (variable)
     * @param {*} data - The corresponding data points for this curve
     * @param {*} opts - Some options for the curve (color, label)
     */
    function addDataset(gindex, key, data, data_opts){
        let graph = graph_array[gindex];
        dataset_to_graph_map[key] = [gindex, graph.addDataset(key, data, data_opts)];
    }

    /**
     * Sets the y-axis of the chart to scale the curves (window matches the maximum amd minimum of all the curves)
     * @param {*} chart - The chart with whihch to scale the y-axis
     * @returns If the minimun y-value of all the curves of the charts is greater than the maximum y-value (same)
     */
    function autoScale(chart) {
        ay = chart.options.scales.y;
        ax = chart.options.scales.x;
        datasets = chart.data.datasets;
        let max = -1e99;
        let min = 1e99;
        // if there are datasets with values and think lines,
        // consider them only. if not, consider all (second pass in the following loop)
        let extraMin = min;
        let extraMax = max;
        for (let i = 0; i < datasets.length; i++){
            ds = datasets[i];
            if (ds.borderWidth == 1) continue;
            let lmax = maxAr(ds.data, ax.min, ax.max);
            let lmin = minAr(ds.data, ax.min, ax.max);
            if(lmax > max)
                max = lmax;
            if(lmin < min)
                min = lmin;
            if (ds.data.length && liveMode) {
                lasty = ds.data.slice(-1)[0].y;
                if (lasty !== null) {
                    extraMin = Math.min(extraMin, lasty);
                    extraMax = Math.max(extraMax, lasty);
                }
            }
        }
        if (min > max) return;
        if (min == max) {
            if (min == 0) {
                ystep = 1;
            } else {
                ystep = Math.abs(min * 0.01);
            }
            min -= ystep;
            max += ystep;
            // chart.graph.setLabelDigits(min, max);
        } else {
            ystep = (max - min) * 0.1;
            chart.graph.setLabelDigits(min, max);
            min -= ystep * 0.02;
            max += ystep * 0.02;
            if (liveMode) {
                extraMin -= ystep;
                extraMax += ystep;
            }
            extraMin = Math.min(min - ystep * 0.5, extraMin);
            extraMax = Math.max(max + ystep * 0.5, extraMax);
            if (min >= ay.min && ay.min >= extraMin &&
                max <= ay.max && ay.max <= extraMax) {
                //console.log('NOCHANGE', max, axis.ticks.max, extraMax)
                return; // do not yet change
            }
            //console.log('CHANMIN', min, axis.ticks.min, extraMin)
            //console.log('CHANMAX', max, axis.ticks.max, extraMax)
            min = extraMin;
            max = extraMax;
        }
        ay.min = min;
        ay.max = max;
    }

    /**
     * Sets the current viewing window to min and max for each graph
     * @param {number} min - The minimum timestamp in milliseconds of the viewing window
     * @param {number} max - The maximum timestamp in milliseconds of the viewing window
     */
    function setMinMax(min, max){
        currentMaxTime = max;
        currentMinTime = min;
        globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime);
        for (let gr of graph_array) {
            if (gr) gr.setMinMax(min, max);
        }
    }

    /** 
     * Adds a data point to the variable's curve
     * @param {string} key - The name of the variable the data point is being added to
     * @param {{x: number, y: number}} data - The new data point to append
     * @returns If the variable is not in the displayed graphs
     *  */ 
    function newDataHandler(key, data){
        if(!(key in dataset_to_graph_map))
            return
        lastTime = Math.max(lastTime, data.x);
        let i = dataset_to_graph_map[key];
        graph_array[i[0]].pushData(i[1], data)
    }

    /**
     * Function called when the graph container is clicked
     * Shows the legend, shows the go to now button, put a cursor then applies the changes
     * If the click is located on the legend, brings it to the front
     * @param {*} evt - The JS event triggered by the click
     */
    function clickHandler(evt) {
        if(evt.target.tagName == "CANVAS"){
            legendFlag = true;
            let trect = evt.target.getBoundingClientRect();
            let X = evt.clientX - trect.x, Y = evt.clientY - trect.y;
            if (X == cursorLinePos) {
               cursorLine(null);
            } else {
               menuGraphicsPopup.hide();
               showLegends(true, false);
               cursorLine(X);
            }
            setLiveMode();
            update();
            for (let gr of graph_array.slice(0, ngraphs)) {
                if (gr && gr.chart.canvas == evt.target) {
                    bringToFront(gr.legend);
                }
                /* clickHandler(evt); */
            }
        }
    }
    function mouseDown(evt) {
        clickMode = 1;
    }
    function mouseUp(evt) {
        if (clickMode == 1) {  // mouse was down, but no pan happend
            clickHandler(evt);
        }
        clickMode = 0;
    }
    // container.addEventListener('click', clickHandler)
    container.addEventListener('mousedown', mouseDown);
    container.addEventListener('mouseup', mouseUp);

    /**
     * Sets (overwrite) the data (curve) of the given variable
     * @param {string} key - The name of the variable to set the data for
     * @param {[{x: number y: number}]} data - The data to set
     * @returns If the variable is not in the graphs
     */
    function setDataFromKey(key, data){
        if(!(key in dataset_to_graph_map))
            return
        let i = dataset_to_graph_map[key];
        graph_array[i[0]].setData(i[1], data);
    }

    /**
     * Gets the available variables
     * @returns An array of strings containing the name of the available variables
     */
    function variables() {
        let vardict = {};
        for (let vars of vars_array) {
            for (let v of vars) {
                vardict[v] = 1;
            }
        }
        return Object.keys(vardict);
    }

    /**
     * Gets the curve for all the variables within a timerange then updates the charts. Called in checkReload
     * This method is almost the same as createGraph. The main difference is that this one does not create new dataset for the variable, but sets its values (overwrite)
     * @param {number} min - The lower bound of the timerange, which is a unix timestamp in milliseconds
     * @param {number} max - The upper bound of previous
     */
    function reloadData(min, max){

        min = min/1000;
        if(max > now()){
            max = 0;
        }else{
            max = max/1000;
        }
        
        timeDelta = currentMaxTime - currentMinTime
        setResolution(timeDelta)

        AJAX("http://" + hostPort + "/graph?time=" + min + ","+max+"&variables=" + variables() + "&interval=" + resolution +  "&id=" + clientID).getJSON().then(function(data){
            for(let key in data.graph){
                let pdata = [];
                for(let e of data.graph[key]){
                    //if(e[0] == null || e[1] == null){
                    //    continue;
                    //}
                    pdata.push({x: e[0]*1000, y: e[1]});
                }
                if(pdata.length > 0){
                    setDataFromKey(key, pdata);
                }
            }
            console.log("RELOAD")
            activateUpdates();

            updateAuto(false);
        });
    }

    /**
     * If the visualisation window is bigger than the time extremas of the curves, ask for the data, then autoscale if needed and apply the changes
     * Called in updateAuto (conditionnal)
     * @param {*} graph - The graph object to check if reload is needed
     * @returns When data is received (no need to autoScale and update as it is done in reloadData)
     */
    function checkReload(graph){
        let ax = graph.chart.options.scales.x;
        let xmin = ax.min, xmax = ax.max;
        /*
        if (xmax < now()-100000) {  // was 100000 = 100sec
            if (liveMode) console.log('UPDATES OFF?')
            //doUpdates = false;
        }else{
            if (!liveMode) console.log('UPDATES ON?')
            //doUpdates = true;
        }*/
        if (xmin < minTime || (!liveMode && xmax > maxTime) || xmax - xmin < 0.5 * (maxTime - minTime)) {
            //TODO:  the criterium for getting finer resolution data should depend, if better res. is available
           // this information has to come from the server
            console.log('reloadData (range change)', xmin - minTime, maxTime - xmax, (xmax - xmin) / (maxTime - minTime))
            reloadData(xmin, xmax);
            minTime = xmin;
            maxTime = xmax;
            return;  // autoScale amd update are done when receiving data
        }
        graph.autoScaleIf();
        graph.update();
    }

    /**
     * Triggers the update of each graph
     */
    function update(){
        for (let gr of graph_array.slice(0, ngraphs)) {
            if (gr) gr.update();
        }
    }

    /**
     * Function called at each mouse wheel step.
     * If zoom mode in x mode, disables liveMode if the visualisaiton window is older than the last known value and sets axis min and max
     * If in y mode, disable the autoscale 
     * then applies the changes
     * @param {*} graph - The graph Object on which the zoom callback has to be called
     */
    function zoomCallback(graph){
        let a, min, max;
        if (currentZoomMode == 'y') {
            a = graph.chart.options.scales.y;
        } else {
            a = graph.chart.options.scales.x;
        }
        min = a.min;
        max = a.max;
        if (!isTouchDevice) {
            /*
            if (prevGraph != graph) {
                prevTime = null;
            }
            if (prevTime !== null) {
                // slow down (relevant when using touch pad)
                fact = (max - min) / (prevMax - prevMin);
                maxFact = 1 + (performance.now() - prevTime) * 0.001;
                let w = 1;
                if (fact > maxFact) {
                   w = (maxFact - 1) / (fact - 1);
                } else if (fact < 1/maxFact) {
                   w = (maxFact - 1) / (1 / fact - 1);
                }
                min = prevMin + (min - prevMin) * w;
                max = prevMax + (max - prevMax) * w;
            }
            prevMin = min; prevMax = max;
            prevTime = performance.now(); prevGraph = graph;
            */
        }
        if (currentZoomMode == 'y') {
            a.min = min;
            a.max = max;
            graph.setAutoScale(false);
        } else {
            if (liveMode && max < lastTime) setLiveMode(false);
            setMinMax(min, max);
        }
        console.log('zoomed')
        update();
    }

    /**
     * Sets the resolution of the viewing window in milliseconds
     * @param {*} timeDelta - The difference between the maximum time and the minimum time of the window
     */
    function setResolution(timeDelta){
        resolution = Math.ceil((timeDelta / container.getBoundingClientRect().width))
    }
    
    /**
     * The callback to be called when the user click on the "Jump" button of the date selector
     * Gets the vars + device name for the selected date+time, then rebuilds the graphs
     * @param {*} dateTimestampMs The user input for the date in ms (Unix timestamp)
     * @param {*} timeValueMs The user input for the time in ms
     */
    function jumpToDate(dateTimestampMs, timeValueMs){
        
        cursorLine(null);
        window["wideGraphs"] = true;
        adjustGrid()
        let msLeftTimestampGetGraph = 0 , msRightTimestampGetVars = 0, msRightTimestampGetGraph = 0;
        
        msLeftTimestampGetGraph = dateTimestampMs;
        
        msRightTimestampGetVars = dateTimestampMs + timeValueMs;
        msRightTimestampGetGraph = dateTimestampMs + 24*60*60*1000;

        AJAX("http://" + hostPort + "/getvars").postForm("time=" + msRightTimestampGetVars/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){
            blocks = data.blocks;
            document.getElementById("device").innerHTML =  data.device
            maxTime = msRightTimestampGetGraph;
            minTime = msLeftTimestampGetGraph;

            currentMaxTime = maxTime;
            currentMinTime = minTime;

            globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime);

            ngraphs = 0; // forces all the graphs to reset
            createGraphs();

            if (liveMode && msRightTimestampGetGraph < lastTime) setLiveMode(false);
        });
       
    }

    /**
     * The function called when the viewing window is moved by the mouse.
     * 
     * Disables live mode if the new max time of the viewing window is lesser than the last known value,
     * then sets the min ans max time axis for all the graphs
     * then applies changes
     * @param {*} graph - The graph for which the function has to be called
     */
    function panCallback(graph){
        let ax = graph.chart.options.scales.x;
        let xmin = ax.min, xmax = ax.max;
        clickMode = 2; // mouse pan mode
        if (liveMode && xmax < lastTime) setLiveMode(false);
        setMinMax(xmin,xmax);
        update();
    }

    /**
     * Sets the viewing window for all graphs if upper bound of the displayed time range is greater than now in live mode,
     * then checks if the data has to be reloaded or simply conditionnaly autoscales then applies the changes
     * @param {*} check - Tells if the data has to be potentially reloaded
     */
    function updateAuto(check=true){
        if (liveMode) {
           max = now();
           if (currentMaxTime && max > currentMaxTime) {
              max = currentMaxTime + Math.min(60000, 0.1 * (currentMaxTime - currentMinTime));
              setMinMax(currentMinTime, max);
              //reloadData(currentMinTime, max);
              //check = false;
           }
        }
        for (let gr of graph_array.slice(0, ngraphs)) {
           if (gr) {
                if (check) { // autoscaleIf and update are done in checkReload
                    checkReload(gr);
                } else {
                    gr.autoScaleIf();
                    gr.update();
                }
            }
        }
    }

    /**
     * Removes the cursor, gets the var and device name for the visualization window of [now-30min, now]) then rebuilds the graphs
     */
    function gotoNow() {
        let msRightTimestamp = graphs.now();
        let msLeftTimestamp = msRightTimestamp - 30*60*1000;
        cursorLine(null);        

        window["wideGraphs"] = false; // will have no effect if hideRightPart is true
        adjustGrid();

        AJAX("http://" + hostPort + "/getvars").postForm("time=" + msRightTimestamp/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){
            currentMaxTime = msRightTimestamp + 60000;
            currentMinTime = msLeftTimestamp;

            maxTime = msRightTimestamp;
            minTime = msLeftTimestamp;           
                 
            blocks = data.blocks;
            document.getElementById("device").innerHTML =  data.device
            ngraphs = 0;
            createGraphs();
            globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime);
            
        });
    }

    /**
     * Builds the main container for the graphs by inserting a slide, and builds the top panel
     */
    function buildGraphicsUI(){

        let f = 0;
        insertSlide(f, " ", "graphics", container);

        let currentSwiper = swiper[f];

        function setSlidingMode(mode) {
            currentSwiper.params.noSwipingClass = mode ? "allow-swipe" : "swiper-slide-main";
        }

        currentSwiper.enableSwiping(false);
        currentSwiper.on('reachBeginning', function () {
            currentSwiper.enableSwiping(false);
        })

        let graphicsPanel = container.parentNode.querySelector('.panel')
        graphicsPanel.classList.add('graphics');
        graphicsPanel.childNodes[0].style.visibility = "hidden"; // hides the span added by the swippers

        loadExportPopup();
        loadCurvesSettingsPopup();
        loadDatesPopup();
        globalIndicators.loadIndicators(graphicsPanel);
        globalControls.loadControls(graphicsPanel);
        loadGraphicsMenu(graphicsPanel);
        
        if (isTouchDevice) {
            doubleTap(removeCursor);
        } else {
            window.addEventListener('dblclick', removeCursor);
            showLegends(true, false);
            adjustLegends();
        }
    }
    /**
     * Inits the graph content
     * @param {[{tag:string, unit:string, curves:[{name:string, label:string, color:string}]}]} blocks - the received blocks
     */
    function initGraphs(blocks){
        buildGraphicsUI();
        receivedVars(blocks);
    }   

    /**
     * Holds the received variables from the /getvars call, gets the server time, then creates the graphs
     * @param {[{tag:string, unit:string, curves:[{name:string, label:string, color:string}]}]} blocks_arg - the received blocks
     */
    function receivedVars(blocks_arg){
        maxTime = timeRange[1]*1000;
        minTime = timeRange[0]*1000;
        if (currentMaxTime == 0) {
            currentMaxTime = maxTime;
            currentMinTime = minTime;
        }
        AJAX("http://" + hostPort + "/gettime?time=-1800,0&id="+ clientID).getJSON().then(function(data){
            startTime = data.time[1]*1000;
            maxTime = startTime;
            currentMaxTime = maxTime + 60000;
            // console.log('MAXTIME', currentMaxTime - Date.now());
            minTime = data.time[0]*1000;
            recvTime = performance.now();

            blocks = blocks_arg;

            createGraphs();

        });
    }

    // Changes the zoom mode icon depending on the current zoom mode
    function setInnerZoomMode(){
        if (currentZoomMode == 'y') {
           globalControls.getControlsMap()[xyKey].changeToAlt();
        } else {
            globalControls.getControlsMap()[xyKey].changeToMain();
        }
        prevTime = null;  // reset zoom speed time
    }

    // Toggles the current zoom mode and sets it for each graph
    function toggleZoomMode(){
        if (currentZoomMode == 'y')
            currentZoomMode = 'x';
        else
            currentZoomMode = 'y';
        setInnerZoomMode();
        for (let gr of graph_array) {
            if (gr) gr.setZoomMode(currentZoomMode);
        }
    }

    function removeCursor(evt=null) {
        graphs.cursorLine(null);
        graphs.update();
    }

    /**
     * New function to activate the graph updates (API call to enbale SSE on the server for the curves)
     */
    function activateUpdates(){

        result = AJAX( "http://" + hostPort +
            "/updategraph?" +
            "id=" + clientID).getJSON().then(function(data) {
            setLiveMode(data.live);
            console.log('LIVE create', liveMode)
        })

    }

    // Obvious
    function getBlocks(){
        return blocks;
    }

    /**
     * Triggers the showLegend method of each graph and updates them if needed
     * @param {*} flag - Tells if the legends has to be shown
     * @param {*} doUpdate - Tells if the graphs have to update
     */
    function showLegends(flag, doUpdate) {
       for (let gr of graph_array) {
           if (gr) {
                gr.showLegend(flag);
                if (doUpdate) gr.update();
            }
        }
        legendFlag = flag;
        //bringToFront(null);
    }

    /**
     * Adjust the legend window depending on the position of the last legend
     */
    function adjustLegends() {
        let lastx = 0, lasty = 0, lasty0 = 0, lasty1 = 0, miny = 0;
        let ys = 22;
        for (let gr of graph_array.slice(0, ngraphs)) {
            if (gr) {
                prect = gr.legend.parentNode.getBoundingClientRect();
                lrect = gr.legend.getBoundingClientRect();
                if (miny == 0) miny = prect.top;
                let x = prect.left, y = Math.max(miny, Math.min(prect.top, prect.bottom - lrect.height));
                if (y < lasty) {
                    x = (lasty0 < lasty1) ? prect.left : lastx;
                }
                gr.positionLegend(x - prect.left, y - prect.top);
                lastx = lrect.right;
                lasty = lrect.bottom;
                if (x < lrect.width / 2) {
                    lasty0 = lasty;
                } else {
                    lasty1 = lasty;
                }
            }
        }
    }

    /**
     * Creates the graphs or selection for each displayed graph
     */
    function createGraphs() {
        let n = Math.min(blocks.length,Math.max(2, Math.floor(window.innerHeight / 200)));

        if (n != ngraphs) {
            for (let i = ngraphs; i < n; i++) {
                if (i >= graph_elm_array.length) {
                    let graph_elm = document.createElement('div');
                    graph_elm.classList.add('graph');
                    graph_elm.classList.add('graph-' + i);
                    container.appendChild(graph_elm);
                    graph_elm_array[i] = graph_elm;
                } else {
                    graph_elm_array[i].style.display = 'flex';
                }
                let shown = false;
                for (block of blocks) {
                    if (block.tag in tag_dict) {
                        idx = tag_dict[block.tag];
                        if (idx < ngraphs) continue;
                    }
                    // this block is either not shown or only in a hidden graph
                    for (let curve of block.curves) {
                        if (curve.show !== false) shown = true;
                    }
                    if (shown) break;
                }
                if (shown) {
                    createGraph(i, blocks[i]);
                } else {
                    createSelection(i);
                }
            }
            for (let i = n; i < graph_elm_array.length; i++) {
                clear(i);
                graph_elm_array[i].style.display = 'none';
            }
            let hs = Math.floor(100 / (n + 1) + 1);
            let h = (100 - (n-1) * hs); // first height
            ngraphs = n;
            for (el of graph_elm_array) {
                el.style.height = h + '%';
                h = hs;
            }
        }
    }

    let cursorElement = document.createElement('div')
    cursorElement.classList.add('cursorline');
    container.appendChild(cursorElement);

    /**
     * Displays the cursor line at the given x position
     * @param {number} setpos - The x position in pixels of the cursor to display
     * @param {boolean} query - (Never set to true in any of the calls)
     * @returns The x position in pixels of the cursor
     */
    function cursorLine(setpos, query=false) {
        if (query) {
            if (!legendFlag) return null;
            if (cursorLinePos === null) return setpos;
            return cursorLinePos;
        }
        cursorLinePos = setpos;
        if (setpos === null) {
            cursorElement.style.display = 'none';
        } else {
            cursorElement.style.display = 'block';
            cursorElement.style.left = (cursorLinePos - 1) + 'px';
            cursorElement.style.height = window.innerHeight + 'px';
        }
        return cursorLinePos;
    }

    /**
     * On resize, recreate the graphs and their legend, amd removes the cursor if displayed
     */
    function resizeHandler() {
      if (blocks) { // prevent error when graphics are not used
        createGraphs();
        adjustLegends();
        if (cursorLinePos) {
            // we do not want to update values -> remove cursor line
            cursorLine(null);
            update();
        }
      }
    }
    window.addEventListener('resize', resizeHandler);

    let frontLegend = null;

    /**
     * Brings the given legend to the front
     * @param {*} legend - The legend to put at the front
     */
    function bringToFront(legend) {
        if (legend != frontLegend) {
            if (frontLegend) {
                frontLegend.style.zIndex = 0;
                //frontLegend.style.opacity = "0.7";
            }
            if (legend) {
                legend.style.zIndex = 1;
                //legend.style.opacity = "1";
            }
            frontLegend = legend;
        }
    }

    function displayExportPopup(){
        exportPopup.show(blocks,currentMinTime, currentMaxTime);
    }

    /**
     * Function called when the user configuration needs to be applied.
     * Calls the /getvars routes with the userconfiguration and reloads all the graphics
     * @param {*} userConfiguration The JSON object representing the user configuration
     */
    function applySettingsCallback(userConfiguration){
        cursorLine(null);

        AJAX("http://" + hostPort + "/getvars").postForm("time=" + currentMaxTime/1000 + "&userconfiguration=" + JSON.stringify(userConfiguration) + "&id="+ clientID).then(function(data){
            blocks = data.blocks;
            document.getElementById("device").innerHTML =  data.device
            maxTime = currentMaxTime;
            minTime = currentMinTime;

            globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime);

            ngraphs = 0; // forces all the graphs to reset
            createGraphs();

            if (liveMode && currentMaxTime < lastTime) setLiveMode(false);
        });
    }

    /**
     * Make sure that the updatAuto() function called in chartJS' onZoomComplete callback is delayed
     * This is needed for mobile phones, as this callback is triggered too often
     */
    function onZoomCompleteCallback(){
        if (updateAutoTimeout === undefined){
            updateAutoTimeout = setTimeout(function() {
            updateAuto();
            updateAutoTimeout = undefined;
            }, 100);
        }
        else{
            clearTimeout(updateAutoTimeout);
            updateAutoTimeout = undefined;
        }
    }

    return {
        addDataset: addDataset,
        newDataHandler: newDataHandler,
        zoomCallback: zoomCallback,
        panCallback: panCallback,
        receivedVars: receivedVars,
        createSelection: createSelection,
        createGraphs: createGraphs,
        doUpdates: function(){return liveMode},
        update: update,
        updateAuto: updateAuto,
        now: now,
        checkReload: checkReload,
        getBlocks: getBlocks,
        createGraph: createGraph,
        autoScale: autoScale,
        showLegends: showLegends,
        setLiveMode: setLiveMode,
        cursorLine: cursorLine,
        bringToFront: bringToFront,

        gotoNow: gotoNow,
        toggleZoomMode: toggleZoomMode,
        jumpToDate: jumpToDate,
        initGraphs: initGraphs,
        onZoomCompleteCallback:onZoomCompleteCallback,

        displayExportPopup:displayExportPopup,
        applySettingsCallback:applySettingsCallback
    }
})();

function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
    let chart;

    let maxspan = 10 * 864e5, minspan = 120000;
    let autoScaleFlag = true;
    let labelDigits = 6, labelMinWidth = 0, labelLongValue = '';

    let parent = document.createElement("div");
    parent.classList.add("chart-container");

    let dselect = document.createElement('div'); //the clickable unit to select the curves we want to display
    dselect.classList.add('dselect');
    dselect.innerHTML = "[" + y_label + "]";
    dselect.addEventListener('click', function(e){
        graphs.createSelection(gindex);
    })
    parent.appendChild(dselect);

    container.appendChild(parent);

    let canvas = document.createElement("canvas");
    canvas.setAttribute("width", "500");
    canvas.setAttribute("height", "500");
    canvas.style.width = "500px";
    canvas.style.height = "500px";
    parent.appendChild(canvas);
    let ctx = canvas.getContext("2d");
    let self = this;

    let chart_options = {
        responsive: true,
        maintainAspectRatio: false,
        animation:{duration:0},
        scales: {
            y:{
                beginAtZero: false,
                ticks:{
                    mirror: true,
                    padding: -10,
                    callback: function(label, index, labels) {
                        if(index == 0 || index == labels.length-1)
                            return "";
                        return strFormat(label);
                    }
                },
                grid:{drawTicks:false},
                title: false, //Former scaleLabel
                type: scaleType,
                position: 'right',
                afterBuildTicks: function(axis) {
                    let ticks = axis.ticks
                    if (scaleType == "logarithmic" && ticks.length <= 4) {
                        y1 = ticks[0];
                        y0 = ticks.slice(-1)[0];
                        span = y1 - y0;
                        step = Math.abs(span * 0.3).toExponential(0);
                        if (step[0] > '5') {
                          step = '5' + step.substr(1);
                        } else if (step[0] > '2') {
                          step = '2' + step.substr(1);
                        }
                        step = Number.parseFloat(step);
                        ticks = [y1];
                        for (let yt = Math.ceil(y1 / step) * step; yt > y0; yt -= step) {
                            ticks.push(yt);
                        }
                        ticks.push(y0);
                    }
                    return ticks
                },
            },
            x:{
                title: false, // former scaleLabel
                type: 'time',
                time: {
                    displayFormats: {'millisecond': 'HH:mm:ss.SSS', 'second': 'HH:mm:ss', 'minute': 'HH:mm','hour': 'EEE d. HH:mm', 'day': 'EE d.', 'week': 'd. MMM yy', 'month': 'MMM yy'},
                },
                ticks: {
                    padding: -20,
                    // stepSize: 180000,
                    autoSkip: true,
                    maxRotation: 0,
                    // callback not used, this is better done in afterBuildTicks
                },
                afterBuildTicks: function(axis) {
                    let ticks = axis.ticks

                    if (!ticks || ticks.length <= 2) return ticks;
                    first = ticks[0].value;
                    step = ticks[1].value - first;
                    offset = (first - axis._adapter.startOf(first, 'day')) % step;
                    let result = [];
                    let v = axis.min;
                    for (tick of ticks) {
                        v = tick.value - offset;
                        if (v > axis.min + step / 2) {
                            result.push({value: v, major: false});
                        }
                    }
                    v += step;
                    if (v < axis.max) result.push({value:v, major: false});
                    axis.ticks = result;
                    // return result;
                },
                beforeFit: function(axis) { // called after ticks are autoskipped
                    prevday = '';
                    for (tick of axis.ticks) {
                        s = tick.label.split(' ');
                        if (s.length == 3) { // format with day
                            // show date only on first label of a day
                            day = s.slice(0, 2).join(' ');
                            if (day != prevday) {
                               prevday = day;
                            } else {
                               tick.label = s[2]; // time
                            }
                        }
                    }
                },
                grid:{drawTicks:false},
            }
        },
        plugins:{
            tooltip: false,
            legend: false,
            zoom:{
                pan: {
                    enabled: true,
                    mode: 'xy',
                    speed: 10,
                    threshold: 10,
                    onPan: function({chart}) { graphs.panCallback(chart.graph);},
                    onPanComplete: function({chart}){graphs.updateAuto();},
                },
                zoom: {
                    wheel:{
                        enabled: true
                    },
                    pinch:{
                        enabled: true
                    },
                    mode: isTouchDevice ? 'xy': 'x',
                    speed: 0.1,
                    sensitivity: 1,
                    onZoom: function({chart}) { graphs.zoomCallback(chart.graph);},
                    onZoomComplete: function({chart}){graphs.onZoomCompleteCallback()},
                }
            }

        }


    }

    if (gindex != 0) {
        // show time labels only on first chart
        chart_options.scales.x.ticks.callback = function () { return ' '; }
    }

    chart  = new Chart(ctx, {type: 'scatter', options: chart_options})

    //console.log('create legend')
    let legend = document.createElement('div');
    legend.classList.add('legend');
    parent.appendChild(legend);

    let legendAnchor = document.createElement('div');
    legendAnchor.classList.add('legendanchor');
    parent.appendChild(legendAnchor);
    let hasLegendAnchor = false;
    //graphSwiper.appendSlide(parent);
    let controls = document.createElement('div');
    controls.classList.add('controls');
    legend.appendChild(controls);

    /**
     * Adds the HTML content into the contols (legend top bar) of each legend, with a callback when the added control is clicked
     * @param {*} inner - The HTML content to add
     * @param {*} callback - The callback triggered on click
     * @returns The added HTML content with its callback
     */
    function addControl(inner, callback){
        let c = document.createElement('div');
        c.classList.add('control');
        //c.classList.add('vcontrol');
        c.innerHTML = inner;
        c.addEventListener('click', function(e){
            if(!legendmoving)
                callback(e);
        })
        controls.appendChild(c);
        let sp = document.createElement('div');
        sp.classList.add('spacer');
        controls.appendChild(sp);
        return c;
    }

    hideBox = document.createElement('div');
    hideBox.innerHTML = '&#215;';
    hideBox.classList.add('control');
    hideBox.classList.add('hidebox');
    hideBox.addEventListener('click', function () {graphs.showLegends(false, true);});

    controls.appendChild(hideBox);

    let autoScaleRow = addControl("<strong>&#9746;</strong> autoscale", function(){
        setAutoScale(!autoScaleFlag);
    });

    let linlog = addControl("<strong>&#9744;</strong> log", function(e){
        //graphs.toggleAxesType();
        toggleAxesType();
    });
    if(scaleType !== "linear"){
        linlog.innerHTML = "<strong>&#9746;</strong> log";
    }

    let tbl = document.createElement('table');
    legend.appendChild(tbl);
    let legendbody = document.createElement('tbody')
    tbl.appendChild(legendbody);
    let legendvalues = {};
    let legendlabels = {};
    let legendlines = {};

    let startX=0,startY=0, startElX=0, startElY=0,legendmoving=false;

    /**
     * Position the legend window at the given x and y (potentially adjusted)
     * @param {number} x - The x position of the top left hand corner
     * @param {number} y - The y position of the top left hand corner
     * @returns If the legend cannot be displayed
     */
    function positionLegend(x, y) {
        let lrect = legend.getBoundingClientRect();
        let prect = parent.getBoundingClientRect();
        ys = 22;

        x = Math.max(0, Math.min(x, prect.width - lrect.width));
        legend.style.left = x + "px";

        /*
        let lim = prect.height - lrect.height;
        Y = Math.max(Math.min(0, lim), Y);
        Y = Math.min(Math.max(lim, 0), Y);
        */
        y = Math.max(ys-lrect.height, Math.min(prect.height-ys, y));
        legend.style.top = y + "px";

        let mid = y + lrect.height / 2;
        if (mid >= 0 && mid <= prect.height) {
            legendAnchor.style.display = 'none';
            hasLegendAnchor = false;
            return;
        }
        if (y < 0) {
            y = Math.min((prect.height - ys) / 2, y + lrect.height - ys);
        } else {
            y = Math.max((prect.height - ys) / 2, y);
        }
        legendAnchor.style.display = 'inline-block';
        legendAnchor.style.left = x + lrect.width + 'px';
        legendAnchor.style.top = y + 'px';
        hasLegendAnchor = true;
    }

    /**
     * The callback called if the legend is moving. Sets the position of the legend at the mouse position.
     * @param {*} e The JS event
     * @return If the mouse does not move enough
     */
    function legendmousemove(e){
        if (Math.abs(e.pageX-startX) + Math.abs(e.pageY-startY) < 4) return;
        legendmoving = true;
        let x = startElX + (e.pageX-startX), y = startElY + (e.pageY-startY);
        positionLegend(x, y);
    }

    /**
     * Removes the different listeners on the legend when the mouse press is finished (within 200ms)
     * @param {*} e - The JS event
     */
    function legendmouseup(e){
        setTimeout(function(){
            legendmoving=false;
        }, 200);
        window.removeEventListener('mousemove', legendmousemove);
        window.removeEventListener('mouseup', legendmouseup);
        window.removeEventListener('blur', legendmouseup);
    }

    // Adds the different event listeners on the legend when it is clicked
    legend.addEventListener('mousedown', function(e){
        if(e.which !== 1){
            return;
        }
        //graphs.bringToFront(legend);
        startX = e.pageX;
        startY = e.pageY;
        startElX = legend.offsetLeft;
        startElY = legend.offsetTop;
        window.addEventListener('mousemove', legendmousemove);
        window.addEventListener('mouseup', legendmouseup);
        window.addEventListener('blur', legendmouseup);
    })

    // Brings the legend to the front is the mouse hovers it
    legend.addEventListener('mouseover', function(e){
        graphs.bringToFront(legend);
    });

    legend.style.display = 'none';

    let margin = 10;

    canvas.addEventListener('mouseover', function(e){
        graphs.bringToFront(legend);
    });

    /**
     * Shows the legend or hide it with the cursor
     * @param {*} flag - Tells if we need to show or note (+ cursor)
     */
    function showLegend(flag){
        if (flag) {
            legend.style.display = 'flex';
            if (hasLegendAnchor)
                legendAnchor.style.display = 'inline-block';
        } else {
            graphs.cursorLine(null);
            legend.style.display = 'none';
            if (hasLegendAnchor)
                legendAnchor.style.display = 'none';
        }
    }

    /**
     * Sets the zoom mode of the current graph
     * @param {string} to - The zoom mode to set
     */
    function setZoomMode(to){
        chart.options.zoom.mode = to;
    }

    // Unused
    function setPanOnOff(to){
        chart.options.pan.enabled = to;
    }

    // Unused
    function addTime(data){
        console.log('OBSOLETE addTime');
        chart.data.labels = data
        chart.update();
    }

    /**
     * Adds a dataset (curve) to the current graph, and adds its legend entry in the graph legend
     * @param {string} key - The curve name (variable)
     * @param {*} data - The corresponding data points for this curve
     * @param {*} opts - Some options for the curve (color, label)
     * @returns The index of this curve in its chart
     */
    function addDataset(key, data, opts){
        let dataset_index = chart.data.datasets.length;
        chart.data.datasets.push({data: data, label: opts.label, key: key,
            spanGaps: false, lineJoin: 'round', borderWidth: 2, steppedLine: opts.period == 0,
            borderColor: opts.color,fill: false, pointRadius: 0, tension:0, showLine: true});

        let dataset = chart.data.datasets[dataset_index];
        let legendrow = document.createElement('tr');
        legendrow.classList.add('legendrow')

        let color = document.createElement('td');
        color.classList.add('semitransparent');
        let dlabel = document.createElement('td');
        dlabel.classList.add('semitransparent');
        let tdvalue = document.createElement('td');

        legendrow.appendChild(color);
        legendrow.appendChild(dlabel);
        legendrow.appendChild(tdvalue);

        let colorline = document.createElement('div');
        color.appendChild(colorline);
        colorline.classList.add('colorline');
        colorline.style.backgroundColor = dataset.borderColor;

        dlabel.innerHTML = dataset.label;
        //dlabel.addEventListener('click', function(evt){
        //    /* dummy listener. really needed ? */
        //});

        let dvalue = document.createElement('div');
        dvalue.classList.add('value');
        tdvalue.appendChild(dvalue);

        legendlines[key] = colorline;
        legendvalues[key] = dvalue;
        legendlabels[key] = dlabel;
        legendrow.addEventListener('click', function(evt){
            if (legendmoving) return;
            for (let k in legendlabels) {
                // set all labels to normal font
                legendlabels[k].style.fontWeight = 400;
            }
            if (evt.target == dlabel) {
                // disable all
                for (let k in legendlines) {
                    legendlines[k].style.height = '1px';
                }
                for (ds of chart.data.datasets) {
                    ds.borderWidth = 1;
                }
                colorline.style.height = '2px';
                dataset.borderWidth = 2;
                dlabel.style.fontWeight = 700; // bold
            } else {
                if (dataset.borderWidth == 1) {
                    colorline.style.height = '2px';
                    dataset.borderWidth = 2;
                } else {
                    colorline.style.height = '1px';
                    dataset.borderWidth = 1;
                    allDeselected = true;
                    for (ds of chart.data.datasets) {
                        if (ds.borderWidth != 1) allDeselected = false;
                    }
                    if (allDeselected) {
                        for (ds of chart.data.datasets) {
                            ds.borderWidth = 2;
                        }
                        for (let k in legendlines) {
                            legendlines[k].style.height = '2px';
                        }
                    }
                }
            }
            console.log('AUTO')
            graphs.autoScale(chart);
            update();
        });
        legendbody.appendChild(legendrow);
        return dataset_index;
    }

    /**
     * Autoscale the graphs or reset the autoscale button of each graph
     * @param {boolean} clear - Tells if the autoscale button has to be reset
     */
    function autoScaleIf(clear=false) {
        if (clear) setAutoScale(false);
        if (autoScaleFlag) graphs.autoScale(chart);
    }

    /**
     * Appends the data_point at the end of the dataset at the given dataset_index in the current Graph
     * @param {number} dataset_index - The index of the dataset (curve) in the current Graph
     * @param {{x: number, y: number}} data_point - The new data point to append
     */
    function pushData(dataset_index, data_point){
        data = chart.data.datasets[dataset_index].data;
        //if (chart.data.datasets[dataset_index].key == 'tt:target')
        //    console.log('BEFORE', data.slice(-3))
        if (data.slice(-1)[0] && data.slice(-1)[0].x >= data_point.x) {
            // replace the last point, when x is lower. this is due to the artifical point added at the end by the live reader
            removed = data.pop();
        }
        data.push(data_point);
    }

    /**
     * Sets (overwrites) the data for a curve (variable)
     * @param {*} index - The index of the curve (variable) in its graph object (Chart)
     * @param {*} data - The data to set at the given index curve
     */
    function setData(index, data){
        chart.data.datasets[index].data = data;
    }

    /**
     * Sets the current viewing window (axis) to min and max (for x values)
     * @param {number} min - The minimum timestamp in milliseconds of the viewing window
     * @param {number} max - The maximum timestamp in milliseconds of the viewing window
     */
    function setMinMax(min, max){
        let ax = chart.options.scales.x;
        let ay = chart.options.scales.y;
        // clamp X-span
        let span = max - min;
        let half = 0;
        if (chart.lastXmin) {
            if (span > maxspan) {
                half = maxspan * 0.5;
            } else if (span < minspan) {
                half = minspan * 0.5;
            }
        }
        if (half) {  // clamped
           mid = (chart.lastXmin + chart.lastXmax) * 0.5;
           min = mid - half;
           max = mid + half;
           ay.min = chart.lastYmin;
           ay.max = chart.lastYmax;
        } else {
           chart.lastXmin = min;
           chart.lastXmax = max;
           chart.lastYmin = ay.min;
           chart.lastYmax = ay.max;
        }
        // custom algorithm for tick step
        mainstep = 1000;
        step = 1000;
        for (info of [['second', 60, [1, 2, 5, 10, 15, 30]],
                      ['minute', 60, [1, 2, 5, 10, 15, 30]],
                      ['hour',  24, [1, 2, 4, 6, 12]],
                      ['day', 365, [1,2,4,7,14,31]]]) {
            if (span < 12 * mainstep * info[2].slice(-1)[0]) {
                for (fact of info[2]) {
                    step = mainstep * fact;
                    if (span < 12 * step) {
                       break;
                    }
                }
                break;
            }
            mainstep *= info[1];
        }
        unit = info[0];
        rstep = Math.round(step / mainstep);
        ax.time.unit = unit;
        ax.ticks.stepSize = rstep;

        ax.max = max;
        ax.min = min;
    }

    /**
     * Triggers the autocale of all the Graphs, then applies the changes to them, and finally updates the HTML autoscale button
     * @param {boolean} flag - Tells if the autoscale has been set or not
     */
    function setAutoScale(flag) {
        autoScaleFlag = flag;
        if (autoScaleFlag) {
            graphs.autoScale(chart);
            update();
            autoScaleRow.innerHTML = "<strong>&#9746;</strong> autoscale";
        } else {
            autoScaleRow.innerHTML = "<strong>&#9744;</strong> autoscale";
        }
    }

    /**
     * Inverts the type of the y-axis (linear <-> logarithmic)
     * Called when log button in the legend is clicked
     */
    function toggleAxesType(){
        setAxesType((chart.options.scales.y.type=== 'linear') ? 'logarithmic' : 'linear');
    }

    /**
     * Sets the y-axis type with a 800ms animation, then applies the changes
     * @param {string} type - The axis type to set
     */
    function setAxesType(type){
        scaleType = type;
        if(type == "linear"){
            linlog.innerHTML = "<strong>&#9744;</strong> log";
        }else{
            linlog.innerHTML = "<strong>&#9746;</strong> log";
        }
        chart.options.scales.y.type = type;
        chart.options.animation.duration = 800;
        if (autoScaleFlag) graphs.autoScale(chart);
        update();
        setTimeout(function(){chart.options.animation.duration = 0;},850)
    }

    /**
     * Updates the single value point in the legend for each curve in each graph based on the position of the cursor.
     * @returns if there is no cursor (so nothing to show)
     */
    function redraw(){
        x = graphs.cursorLine(canvas.clientWidth, true);
        if (x === null) return;
        for(let i in chart.data.datasets){
            let y = null;
            for(let j = 0; j < chart.getDatasetMeta(i).data.length; j++){
                let dp = chart.getDatasetMeta(i).data[j];
                if (dp.x >= x) break; //_model does not exist anymore, properties are defined directly on elements
                y = chart.data.datasets[i].data[dp.$context.index].y; // $context not mentionned in ChartJS doc, seen with console.log
            }
            valueElm = legendvalues[chart.data.datasets[i].key];
            if (labelMinWidth == 0) {
                valueElm.style.minWidth = '0px';
                valueElm.innerHTML = labelLongValue;
                labelMinWidth = valueElm.clientWidth;
                valueElm.style.minWidth = labelMinWidth + 'px';
            }
            if (y !== null) {
                valueElm.innerHTML = strFormat(y, labelDigits);
            }
        }

        /*
        //console.log('REDRAW', dselect.innerHTML, x);
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, canvas.height);
        ctx.lineWidth = 2;
        ctx.strokeStyle = "rgba(0,0,0,0.8)";
        //ctx.globalCompositeOperation = "destination-over";
        ctx.globalCompositeOperation = "source-over";
        ctx.stroke();
        ctx.globalCompositeOperation = "source-over";
        */
    }

    /**
     * Applies the changes to the chart object and redraws the legend (single value)
     */
    function update(){
        chart.update();
        redraw();
    }
    /**
     * Sets the digits of the legend labels depending on the extremas of the curves for this graph
     * @param {number} min - The mimimum y-value of all the curves of the graph
     * @param {number} max - The maximum y-value of all the curves of the graph
     */
    function setLabelDigits(min, max) {
        let dig = parseInt(Math.max(-min, max) / (max - min)).toFixed(0).length + 3;
        let l = 0;
        for (let val of [min, max]) {
            evalue = val.toExponential(dig - 1).replace("e+", "e");
            fvalue = val.toFixed(Math.max(0, dig - Math.abs(parseInt(val)).toFixed().length));
            if (evalue.length < fvalue.length) fvalue = evalue;
            if (fvalue.length > l) {
                l = fvalue.length;
                labelMinWidth = 0;
                labelLongValue = fvalue;
            }
        }
        labelDigits = dig;
    }

    self = {
        addTime: addTime,
        addDataset: addDataset,
        pushData: pushData,
        setMinMax: setMinMax,
        setAxesType: setAxesType,
        setZoomMode: setZoomMode,
        showLegend: showLegend,
        setPanOnOff:setPanOnOff,
        redraw: redraw,
        update: update,
        setData: setData,
        autoScaleIf: autoScaleIf,
        setAutoScale: setAutoScale,
        positionLegend: positionLegend,
        setLabelDigits: setLabelDigits,
        chart: chart,
        legend: legend,
    }

    chart.graph = self;
    return self;
}

/** 
 * Called when new data is received from the server (SSE).
 * Appends new data to the corresponding curves' datasets
 * @param {{type:"graph-update", time:number, graph:{string:[[(number),(number)]]}}} graph - The graph response from the server
 */
function updateCharts2(graph){
    if(!graphs.doUpdates()) {
        console.log('graphs.doUpdates skipped');
        return;
    }
    //console.log('G', graph);
    for(let key in graph){
        if (graph[key][0] != null) {
            // there is at least ONE valid datapoint
            for (pt of graph[key]) {
                graphs.newDataHandler(key, {x: pt[0]*1000, y: pt[1]});
            }
        }
    }
    graphs.updateAuto(false);
    // graphs.update();
}

/** Useless (called in graph-draw response case in SEAWebClientCommunication) */
function createCharts2(arg) {
    console.log('C2', arg)
}
