/*globals console, angular, d3, dispatch, FFT, bson*/

// https://github.com/mongodb/js-bson
angular.module('IOLabApp').factory('BSON', function () {
    'use strict';

    var exports = bson().BSON;

    exports.serialize2 = function (object, checkKeys, asBuffer, serializeFunctions) {
        var size = exports.calculateObjectSize(object, serializeFunctions);
        var buffer = new Array(size);
        exports.serializeWithBufferAndIndex(object, checkKeys, buffer, 0, serializeFunctions);
        return buffer;
    };

    return exports;
});

angular.module('IOLabApp').factory('IOLab', function ($rootScope, BSON, IOLabAPI) {
    'use strict';

    /*
     * Private Members
     */

    var hardwareState = {
        remote1: { rfStatus: 0, configured: false, fixedConfig: 0 },
        remote2: { rfStatus: 0, configured: false, fixedConfig: 0 }
    };
    var acquisitionStack = { 'default': {
        remote1: { sensors: [] },
        remote2: { sensors: [] }
    }};

    var exports = {},
        apply = function (fct) {
            return $rootScope.$$phase ? fct() : $rootScope.$apply(function () { fct(); });
        };

    // Treatment of C++ signals
    exports.execute = function (method, args) {
        switch (method) {

        case 'hardwareStateChanged':
            exports.getHardwareState();
            break;

        case 'acquisitionStackChanged':
            exports.getAcquisitionStack();
            break;

        case 'dataStarted':
            apply(function () { $rootScope.$broadcast('dataStarted', ""); });
            break;

        case 'dataStopped':
            apply(function () { $rootScope.$broadcast('dataStopped', ""); });
            break;

        case 'fixedConfigChanged':
            apply(function () { $rootScope.$broadcast('fixedConfigChanged', ""); });
            break;
        }
    };

    /*
     * Public methods
     */

    // Return true when the acquisition is set and has no data!
    exports.isNewAcquisition = function () {
        return acquisitionStack['default'].lastStartDate == 'not-a-date-time';
    };

    exports.returnHardwareState = function () { return hardwareState; };

    exports.returnAcquisitionStack = function () { return acquisitionStack; };

    exports.sendCommand = function (cmd) {
        IOLabAPI.sendCommand(cmd);
    };

    exports.powerOff = function (remote) {
        IOLabAPI.remotePowerOff(remote);
    };

    exports.getHardwareState = function () {
        IOLabAPI.getHardwareState(function (bson) {
            hardwareState = BSON.deserialize(bson);
            apply(function () {
                $rootScope.$broadcast('hardwareStateChanged', "");
            });
        });
    };

    exports.getAcquisitionStack = function () {
        IOLabAPI.getAcquisitionStack(function (bson) {
            acquisitionStack = BSON.deserialize(bson);
            apply(function () {
                $rootScope.$broadcast('acquisitionStackChanged', "");
            });
        });
    };

    exports.pairRemote = function (remote, status) {
        IOLabAPI.pairRemote(remote, status);
    };

    exports.setFixedConfig = function (remote, config, force) {
        IOLabAPI.setFixedConfig(remote, config, force || false);
    };

    exports.startData = function () {
        IOLabAPI.startDataAcquisition();
    };

    exports.stopData = function () {
        IOLabAPI.stopDataAcquisition();
    };

    exports.saveCalibration = function (json) {
        var bsrec = BSON.serialize2(json);
        IOLabAPI.saveBson("Calibration", bsrec);
    };

    exports.saveBson = function (collection, json) {
        var bsrec = BSON.serialize2(json);
        IOLabAPI.saveBson(collection, bsrec);
    };

    exports.executeQuery = function (callback, collection, json) {
        var bsrec = BSON.serialize2(json);
        IOLabAPI.executeQuery(callback, collection, bsrec);
    };

    exports.calibrateSensor = function (remote, sensor) {
        return IOLabAPI.calibrateSensor(remote, sensor);
    };

    exports.resetSensorOffset = function (remote, sensor) {
        return IOLabAPI.resetSensorOffset(remote, sensor);
    };

    exports.exportData = function (remote, sensor, fileName) {
        return IOLabAPI.exportData(remote, sensor, fileName || "");
    };

    exports.getSensorData = function (callback, args) {
        return IOLabAPI.getSensorData(
            callback,
            [args.remote, args.sensor, args.startTime || 0, args.endTime || -1, args.raw || false ]
        );
    };

    exports.getSensorData2 = function (callback, args) {
        return IOLabAPI.getSensorData2(
            callback,
            [args.remote, args.sensor1, args.sensor2, args.startTime || 0, args.endTime || -1]
        );
    };

    /* AcquisitionStack management */

    exports.stackAcquisition = function () {
        IOLabAPI.stackAcquisition();
    };

    exports.deleteAcquisition = function () {
        IOLabAPI.deleteAcquisition();
    };

    return exports;
});

angular.module('IOLabApp').factory('sensorSelectionService', function($rootScope, IOLab, configService) {
    'use strict';

    var exports = {},
        sensorConfigs = configService.getSensorConfigs(),
        selectedConfig = [0, 0];

    // Array to containing info for the remote
    exports.sensors = [];
    // Non available sensors () for the configurations compatible with the selected sensors
    exports.notAvailable = [];
    // Rate for sensors in the selected config
    exports.sampleRates = [];
    // Selected sensors
    exports.checked = [];

    // Returns the available configurations depending
    // on the selected sensors
    function getAvailableConfigs(remote) {
        var configs = [];
        var availableConfigs = sensorConfigs;
        var ckValueTrueCount = 0;       // Counts if any true values exist in the $scope.checked array

        angular.forEach(exports.checked[remote], function (ckValue, ckKey) {
            // ckKey represents the sensor code
            // ckValue is true if the ckKey sensor is checked, false otherwise
            if (ckValue) {
                // Reset the configs array
                configs = [];
                var tempAvailableConfigs = [];

                angular.forEach(availableConfigs, function (value) {
                    // value represents an object containing a configKey, sensorKey, and sampleRate.
                    if (value.sensorKey == ckKey) {
                        this.push(value.configKey);
                    }
                }, configs);

                // Scan through all remaining configurations (an array)
                angular.forEach(configs, function (cValue) {
                    // Scan through all the previously known configurations (an object)
                    angular.forEach(availableConfigs, function (value) {
                        if (value.configKey == cValue) {
                            // Add to the tempAvailableConfigs array (as an object) possible configurations
                            tempAvailableConfigs.push(value);
                        }
                    });
                });

                // Update the availableConfigs object
                availableConfigs = tempAvailableConfigs;
                ckValueTrueCount = ckValueTrueCount + 1;
            }

            if (ckValueTrueCount == 0) {
                angular.forEach(availableConfigs, function (value) {
                    // value represents an object containing a configKey, sensorKey, and sampleRate.
                    this.push(value.configKey);
                }, configs);
            }
        });

        return configs;
    }

    // Returns the list of remaining sensors compatible with the
    // selected ones) taking into account the possible fixedConfigs.
    function getRemainingSensors(availableConfigs) {
        var sensors = [];

        angular.forEach(availableConfigs, function (aCvalue) {
            angular.forEach(sensorConfigs, function (sCvalue, sCkey) {
                if (sensorConfigs[sCkey].configKey == aCvalue) {
                    sensors.push(sensorConfigs[sCkey].sensorKey);
                }
            });
        });
        // The return will send back an ordered array with no duplicates
        return sensors.slice().sort(function (a, b) { return a - b; })
            .reduce(function (a, b) { if (a.slice(-1) != b) a.push(b); return a; }, []);
    }

    // Select the configuration (with the highest frequency)
    function selectConfig(remote, availableConfigs) {
        var firstSelectedSensor = exports.checked[remote].indexOf(true);
        var highestRate = 0;
        var configs = [];
        var rates = [];
        selectedConfig[remote] = 0;

        // Fill the configs array with possible configuration keys and sample rates for the firstSelectedSensor
        angular.forEach(availableConfigs, function (aCvalue) {
            angular.forEach(sensorConfigs, function (sCvalue) {
                if (aCvalue == sCvalue.configKey && sCvalue.sensorKey == firstSelectedSensor) {
                    configs.push({ configKey: sCvalue.configKey, sampleRate: sCvalue.sampleRate });
                }
            });
        });

        // Go through the configs array and find the highest sample rate and its corresponding config key
        angular.forEach(configs, function (value) {
            if (parseInt(value.sampleRate, 10) > parseInt(highestRate, 10)) {
                highestRate = value.sampleRate;
                selectedConfig[remote] = value.configKey;
            }
        });

        // Go through the sensorConfigs object and fill the rates array with checked sensors' rates,
        // where the key to the rates array corresponds to the sensor key
        angular.forEach(sensorConfigs, function (value) {
            if (value.configKey == selectedConfig[remote]) {
                rates[value.sensorKey] = value.sampleRate;
            }
        });

        return rates;
    }

    // Update the notAvailable array depending on the sampleRates available
    // (that correspond to the the selected config !)
    function updateNotAvailable(remote) {
        angular.forEach(exports.sensors[remote], function(sensor) {
            exports.notAvailable[remote][sensor.code] = !exports.sampleRates[remote][sensor.code];
        })
    }

    // Initialisation (to be done when the service is instanciated)
    function init(remote) {
        var acquisitionStack = IOLab.returnAcquisitionStack()['default'],
            currentConfig = (acquisitionStack['remote' + remote] || {}).config || 0;

        if (currentConfig !== 0) {
            selectedConfig[remote] = currentConfig;

            // Set the sampleRates array (frequency of sensors of the selected config)
            angular.forEach(sensorConfigs, function (value) {
                if (value.configKey == selectedConfig[remote]) {
                    exports.sampleRates[remote][value.sensorKey] = value.sampleRate;
                }
            });

            // Set the notAvailable sensors array
            updateNotAvailable(remote);
        }
    }

    exports.reset = function (remote) {
        if (arguments.length == 0) {
            exports.reset(1);
            exports.reset(2);
            return;
        }

        exports.sensors[remote] = configService.getSensors();
        exports.notAvailable[remote] = [];
        exports.sampleRates[remote] = [];

        exports.checked[remote] = [];
        angular.forEach(exports.sensors[remote], function (value) {
            exports.checked[remote][value.code] = false;
        });
    };

    exports.checkFixConfig = function (remote) {

        if (IOLab.isNewAcquisition()) {
            var acquisitionStack = IOLab.returnAcquisitionStack()['default'],
                currentConfig = (acquisitionStack['remote' + remote] || {}).config || 0;

            if (currentConfig == selectedConfig[remote] && currentConfig != 0) {
                $rootScope.$broadcast('updateSensors' + remote);
            } else if (selectedConfig[remote] != 0) {
                IOLab.setFixedConfig(remote, parseInt(selectedConfig[remote], 10), true);
            } else if (currentConfig != 0) {
                IOLab.deleteAcquisition();
            }
        }
    };

    exports.sensorChecked = function (remote, sensor) {

        if (IOLab.isNewAcquisition()) {
            exports.checked[remote][sensor.code] = !exports.checked[remote][sensor.code];

            // Initialize the sensors array
            angular.forEach(exports.sensors[remote], function (value) {
                exports.notAvailable[remote][value.code] = true;
                exports.sampleRates[remote][value.code] = "";
            });

            // Retrieve the available configurations with the selected sensor
            var availableConfigs = getAvailableConfigs(remote);

            exports.sampleRates[remote] = selectConfig(remote, availableConfigs);

            // Now retrieve the available sensors that can use the remaining configurations
            var availableSensors = getRemainingSensors(availableConfigs);

            angular.forEach(availableSensors, function (value) {
                exports.notAvailable[remote][value] = false;
            });

            exports.checkFixConfig(remote);
        }
        if(!IOLab.isNewAcquisition() && exports.sampleRates[remote][sensor.code]) {
            exports.checked[remote][sensor.code] = !exports.checked[remote][sensor.code];
            $rootScope.$broadcast('updateSensors' + remote);
        }
    };

    // Set the sensors not in the current config not available when entering in data mode
    $rootScope.$on('hardwareStateChanged', function() {
        if (IOLab.returnHardwareState().dongleMode === 16 /* data */) {
            if (exports.checked[1].indexOf(true) > -1) {
                updateNotAvailable(1);
            }
            if (exports.checked[2].indexOf(true) > -1) {
                updateNotAvailable(2);
            }
        }
    });

    // Initialisation of the service
    exports.reset();
    init(1);
    init(2);

    return exports;
});

angular.module('IOLabApp').factory('chartService', function (helperService, configService, chartModeService, selectionService) {
    'use strict';

    var chart = function (args) {

        var remote = args.remote,
            config = args.config,
            sensor = args.sensorType,
            chartID = remote + '-' + config + '-' + sensor,
            param = configService.getSensor(sensor) || {};

        // Default width and height (setter/getter)
        var width = 550, height = 250;

        // Default margin
        var margin = { top: 20, right: 20, bottom: 30, left: 30 };

        // Scale Factors
        var scaleFactors = param.scaleFactors || [1];

        // Chart scales' range
        var defScale = { x0: 0, x1: 10, y0: -20, y1: 20 };
        if (param.scales) {
            defScale.y0 = param.scales[0];
            defScale.y1 = param.scales[1];
        }
        var scale = Object.create(defScale);

        // Chart paths (used to show/hide paths)
        var chartPaths = [];

        // Sampling control variables
        var samplingRate = 1, frequency = 100;

        // FFT sampling rate and spectrum
        var fftSamplingRate = null,
            fftResult = null,
            fftLocked = false,
            d3Mouse = null,
            d3MouseDown = null;

        // Time average on side points (total points for smoothing 2 * sidePoints + 1 )
        var sidePoints = 0;

        // Cursor position from acquiCtrl (ms)
        var cursorPosition = -1;
        var cursorRange = {t0: -1, t1: -1};

        /*
         * Cursor selection object
         */
        var selection = selectionService();

        /*
         * Chart Mode object
         */
        var chartMode = chartModeService();

        // Variable containing the data to bind. Used also to get the
        // value of t, x1, x2, ... when hovering
        var data = [], dataOrig = [];

        // D3 object for the chart rendering
        var svg, chartW, chartH, xScale, yScale, xAxis, yAxis, line, area;

        var dispatch = d3.dispatch(
            'cursorPosition',
            'cursorRange',
            'fftResult',
            'fftInit',
            'scaleChanges',
            'setMode'
        );

        var resize = {
            active: false,
            d3MouseDown: null,
            d3MouseElement: null
        };
        resize.mousedown = function () {
            resize.active = true;
            resize.d3MouseElement = this;
            resize.d3MouseDown = d3.mouse(this);
        };
        resize.mousemove = function () {
            if (!resize.active) {
                return;
            }
            d3Mouse = d3.mouse(resize.d3MouseElement);
            width += d3Mouse[0] - resize.d3MouseDown[0];
            height += d3Mouse[1] - resize.d3MouseDown[1];
            resize.d3MouseDown = d3Mouse;
            sampleRateCalc();
            redraw();
        };
        resize.mouseup = function () {
            resize.active = false;
            resize.d3MouseDown = null;
        };

        var mousedown = function () {
            // Needed to check the click at the same position
            d3MouseDown = d3.mouse(this);

            if (selection.p0.locked) {
                selection.reset();
                if (chartMode.zoom) {
                    redrawZoomSelection();
                }
            }

            if (chartMode.stats || chartMode.zoom || chartMode.move) {

                // Keep a copy of the scale
                selection.scale = angular.copy(scale);

                // Lock p0 and keep the value
                selection.p0.locked = true;
                selection.p0.x = xScale.invert(d3MouseDown[0]);
                selection.p0.y = yScale.invert(d3MouseDown[1]);
            }
        };

        var mousemove = function () {
            // Index of the data point corresponding to the mouse position
            d3Mouse = d3.mouse(this);

            // Selection locked (update the selection)
            if (selection.p0.locked) {

                selection.p1.x = xScale.invert(d3Mouse[0]);
                selection.p1.y = yScale.invert(d3Mouse[1]);

                if (chartMode.stats) {
                    dispatch.cursorRange(
                        {t0: parseFloat(selection.p0.x), t1: parseFloat(selection.p1.x)}
                    );
                }

                if (chartMode.move) {
                    scale.x0 -= (selection.p1.x - selection.p0.x);
                    scale.x1 -= (selection.p1.x - selection.p0.x);
                    scale.y0 -= (selection.p1.y - selection.p0.y);
                    scale.y1 -= (selection.p1.y - selection.p0.y);
                    redraw();
                    dispatch.scaleChanges({ x0: scale.x0, x1: scale.x1 });
                }
            }

            // Dispatch the time position to the controllers
            // The time value us the exact time in a data set
            // When receiving the position from an other chart
            // or remote the time should be adjusted...
            if (chartMode.stats) {
                dispatch.cursorPosition(xScale.invert(d3Mouse[0]));
            } else {
                dispatch.cursorPosition(-1);
            }

            if (chartMode.zoom) {
                redrawZoomSelection();
            }

            // FFT management
            if (chartMode.stats && fftSamplingRate && !fftLocked) {
                var index = helperService.getDataIndex(data, xScale.invert(d3Mouse[0]));
                computeFftSpectrum(index);
                redrawFftSelection();
            }
        };

        var mouseout = function () {
            // Lock fft result when exiting
            if (fftResult && !fftLocked) {
                fftLocked = true;
            }

            // Indicate that the cursor is out of the chart
            dispatch.cursorPosition(-1);
            redrawLegend();

            // Erase the cross ...
            d3Mouse = null;
            redrawZoomSelection();
        };

        var mouseup = function () {
            // Set the new scale in zoom mode
            if (chartMode.zoom && selection.notNull()) {
                // Keep the old scale for further use
                chartMode.scales.push(angular.copy(scale));

                scale.x0 = Math.min(selection.p0.x, selection.p1.x);
                scale.x1 = Math.max(selection.p0.x, selection.p1.x);
                scale.y0 = Math.min(selection.p0.y, selection.p1.y);
                scale.y1 = Math.max(selection.p0.y, selection.p1.y);
                selection.reset();
                sampleRateCalc();
                redraw();
                redrawZoomSelection();
                dispatch.scaleChanges({ x0: scale.x0, x1: scale.x1 });
            }

            selection.reset();
        };

        var click = function () {
            if (chartMode.stats && fftResult && d3MouseDown[0] === d3.mouse(this)[0]) {
                fftLocked = !fftLocked;
            }

            var scalesLen = chartMode.scales.length;

            if (chartMode.zoom && !!scalesLen && d3MouseDown[0] === d3.mouse(this)[0]) {
                scale = chartMode.scales.splice(-1, 1)[0];
                sampleRateCalc();
                redraw();
                redrawZoomSelection();
                dispatch.scaleChanges({ x0: scale.x0, x1: scale.x1 });
            }
        };

        var dblclick = function () {

            // Reset stats
            if (chartMode.stats) {
                dispatch.cursorRange({ t0: -1, t1: -1});
            }

            // Reset the zoom scale to the bbox
            if (chartMode.zoom) {
                scale = helperService.bbox(dataOrig, scale, param);
                chartMode.scales = [];
                sampleRateCalc();
                redraw();
                dispatch.scaleChanges({ x0: scale.x0, x1: scale.x1 });
            }
        };

        var showPath = function (i) {
            // Always show path if chartPaths is empty...
            return chartPaths.length == 0 || (chartPaths[i] && chartPaths[i].show);
        };

        var redrawLegend = function () {
            if (cursorPosition == -1) {
                svg.select('.legend-group').style('display', 'none');
                return;
            }

            // Index of the data point corresponding to the mouse
            // position from the controller
            var index = helperService.getDataIndex(data, cursorPosition);

            // If no data (index = -1) => return
            if (index < 0 || data.length == 0) {
                return;
            }

            // Build data array to render [t, x0, x1, ...]
            var svgData = [data[0][index][0]];
            // x0, x1, ...
            data.forEach(function (entry) { return svgData.push(entry[index][1]); });

            // Activate the display of the legend
            svg.select('.legend-group').style('display', null);

            // Update/create the sensor values
            svg.select('.legend-group')
                .selectAll('.legend-text')
                  .data(svgData)
                  .attr('x', 10)
                  .attr('y', yTextPosition)
                  .text(legendText)
                .enter().append('text')
                  .classed('legend-text', true)
                  .attr('fill', legendColors);

            // Update/create a line for the time position
            svg.select('.legend-group')
                .selectAll('.legend-lineX')
                  .data([svgData[0]])  // data = [t]
                  .attr('x1', function (d) { return xScale(d); })
                  .attr('x2', function (d) { return xScale(d); })
                  .attr('y2', chartH)
                  .attr('y1', 0)
                .enter().append('line')
                  .classed('legend-lineX', true)
                  .attr({'stroke-width': 1, 'stroke': '#555555'});
        };

        var redrawStatistics = function () {
            if (cursorRange.t0 == -1 && cursorRange.t1 == -1) {
                svg.select('.statistics-group').style('display', 'none');
                return;
            }

            var index0 = helperService.getDataIndex(data, Math.min(cursorRange.t0, cursorRange.t1));
            var index1 = helperService.getDataIndex(data, Math.max(cursorRange.t0, cursorRange.t1));

            if (index0 < 0 || index1 < 0) {
                return;
            }

            // Data set on which we will compute the statistics
            var dataSet = [];
            data.forEach(function (entry) {
                dataSet.push(entry.slice(index0, index1 + 1));
            });
            var dataSetLen = dataSet[0].length;

            // Initialization of the object containing the statistics
            var dataStatistics = { mean: [], sigma: [], area: [] };
            dataStatistics.delta_t = Math.abs(dataSet[0][Math.max(dataSetLen - 1, 0)][0] - dataSet[0][0][0]);

            // Mean & Standard Deviation calculation (corrected sample standard deviation)
            dataSet.forEach(function (entry) {
                var mean = d3.mean(entry, function (d) {
                    return d[1];
                });

                var variance = d3.sum(entry, function (d) {
                    return (d[1] - mean) * (d[1] - mean);
                }) / (dataSetLen - 1);

                dataStatistics.mean.push(mean);
                dataStatistics.sigma.push(Math.sqrt(variance));
                dataStatistics.area.push(mean * dataStatistics.delta_t);
            });


            // Activate the display of the stat
            svg.select('.statistics-group').style('display', null);

            // Build data array to render [∆t, [m0, s0, a0], [m1, s1, a1], ...]
            var svgData = [], i;
            svgData.push(dataStatistics.delta_t);
            for (i = 0; i < data.length; i++) {
                svgData.push([dataStatistics.mean[i], dataStatistics.sigma[i], dataStatistics.area[i]]);
            }

            // Update/create the svg text
            svg.select('.statistics-group')
                .selectAll('.stat-text')
                  .data(svgData)
                  .attr('x', chartW - 280)
                  .attr('y', yTextPosition)
                  .text(statText)
                .enter().append('text')
                  .attr('class', 'stat-text')
                  .attr('fill', legendColors);

            // xScale on the start and end time of the data set
            svgData = [[xScale(dataSet[0][0][0]), xScale(dataSet[0][dataSetLen - 1][0])]];

            // Update/create a svg rectangle
            svg.select('.statistics-group')
                .selectAll('.stat-rect')
                  .data(svgData)
                  .attr('x', function (d) { return Math.min(d[0], d[1]); })
                  .attr('y', 0)
                  .attr('height', chartH)
                  .attr('width', function (d) { return Math.abs(d[0] - d[1]); })
                .enter().append('rect')
                  .attr('class', 'stat-rect')
                  .attr('fill', '#d0b31d')
                  .attr('fill-opacity', 0.1)
                  .attr('clip-path', 'url(#clip-' + chartID + ')');

            // Area under the curve
            var areaContainer = svg.select('.statistics-group')
                .selectAll('.stat-area')
                .data(dataSet)
                .attr('d', area)
                .style('display', function (d, i) {
                    return (!showPath(i) ? 'none' : null);
                });
            areaContainer.enter().append('path')
                  .attr('class', 'stat-area')
                  .attr('fill', pathColors)
                  .attr('fill-opacity', 0.3)
                  .attr('clip-path', 'url(#clip-' + chartID + ')');
            areaContainer.exit().remove();
        };

        var redrawZoomSelection = function () {

            /*
             * Cross
             */
            var crossData = chartMode.zoom && d3Mouse? [d3Mouse[0], d3Mouse[1]] : [],
                crossGroup;

            crossGroup = svg.select('.zoom-group')
                .selectAll('.zoom-cross')
                .data(crossData)  // data = [y]
                .attr('x1', function (d, i) { return i ? 1 : d; })
                .attr('x2', function (d, i) { return i ? chartW : d; })
                .attr('y2', function (d, i) { return i ? d : chartH; })
                .attr('y1', function (d, i) { return i ? d : 0; });
            crossGroup.enter()
                .append('line')
                .attr('class', 'zoom-cross')
                .attr({'stroke-width': 1, 'stroke': '#555555'});
            crossGroup.exit().remove();


            /*
             * Rectangle
             */
            var zoomData, zoomGroup;

            if (selection.p0.x !== -1 && selection.p1.x !== -1) {
                zoomData = [{
                    x0: selection.p0.x < selection.p1.x ? selection.p0.x : selection.p1.x,
                    x1: selection.p0.x < selection.p1.x ? selection.p1.x : selection.p0.x,
                    y0: selection.p0.y < selection.p1.y ? selection.p0.y : selection.p1.y,
                    y1: selection.p0.y < selection.p1.y ? selection.p1.y : selection.p0.y
                }];
            } else {
                zoomData = [];
            }

            zoomGroup = svg.select('.zoom-group')
                .selectAll('.zoom-rect')
                .data(zoomData)
                .attr('x', function (d) { return xScale(d.x0); })
                .attr('y', function (d) { return yScale(d.y1); })
                .attr('width', function (d) { return xScale(d.x1) - xScale(d.x0); })
                .attr('height', function (d) { return yScale(d.y0) - yScale(d.y1); });
            zoomGroup.enter()
                .append('rect')
                .attr('class', 'zoom-rect')
                .style({fill: 'none', stroke: '#555555', 'stroke-width': 1.5, 'stroke-dasharray': '6,2'});
            zoomGroup.exit().remove();
        };

        // Build Y position:
        var yTextPosition = function (d, i) {
            var yPosition = -3, n;
            for (n = 1; n <= i; n++) {
                if (showPath(n - 1)) {
                    yPosition += 13;
                }
            }
            return yPosition;
        };

        var legendText = function (d, i) {
            if (i == 0) {
                return 't: ' + d.toFixed(5) + ' s';
            }
            if (!showPath(i - 1)) {
                return '';
            }
            var legend = param.legends ? (' ' + param.legends[i - 1]) : ('x' + i),
                unit = param.units ? (' ' +  param.units[i - 1]) : '';

            return legend + ': ' + d.toFixed(2) + unit;
        };

        var statText = function (d, i) {
            if (i == 0) {
                return '∆t: ' + d.toFixed(5) + ' s';
            }
            if (!showPath(i - 1)) {
                return '';
            }
            var space = param.units ? ' ' : '',
                unit = param.units ? (param.units[i - 1]) : '',
                areaUnit = configService.getAreaUnit(unit);

            return 'μ: ' + d[0].toFixed(2) + space + unit
               + ' — σ: ' + d[1].toPrecision(2) + space + unit
               + ' — Area: ' + d[2].toFixed(2) + space + areaUnit;
        };

        var legendColors = function (d, i) {
            if (i === 0) {
                return '#444444';
            }
            return pathColors(d, i - 1);
        };

        var pathColors = function (d, i) {
            if (param.pathColors) {
                return param.pathColors[i];
            }
            return configService.getDefaultColor(i);
        };

        var sampleRateCalc = function (dataFrequency) {
            if (dataFrequency) {
                frequency = dataFrequency
            }
            samplingRate = Math.max(1, Math.floor(frequency * (scale.x1 - scale.x0) / chartW));
        };

        var showHidePaths = function () {
            if (!chartPaths) {
                return;
            }
            chartPaths.forEach(function (path, idx) {
                svg.select('.chart-group').selectAll('.x' + idx)
                    .style('display', (chartPaths[idx].show ? null : 'none'));
            });
        };

        var smoothingPath = function (newSidePoints) {
            if (!data.length ||
                    newSidePoints === null ||
                    newSidePoints === sidePoints) {
                return;
            }

            if (sidePoints !== 0 && newSidePoints === 0) {
                sidePoints = 0;
                helperService.copyData(dataOrig, data);
            }

            if (newSidePoints !== 0) {
                sidePoints = newSidePoints;
                helperService.smoothData(dataOrig, data, sidePoints);
            }

            // Data rebinding !!!!
            var paths = svg.select('.chart-group')
                .selectAll('.paths')
                .data(data);
        };

        var computeFftSpectrum = function (index) {

            if (fftSamplingRate === null) {
                fftResult = null;
            } else if (index || (fftResult && fftResult.index)) {
                fftResult =
                    helperService.computeFFT(data, index || (fftResult && fftResult.index), fftSamplingRate);
            }
        };

        var redrawFftSelection = function () {
            var fftSelection = [];

            if (fftResult !== null) {
                fftSelection = [[
                    xScale(data[0][fftResult.index - fftSamplingRate / 2][0]),
                    xScale(data[0][fftResult.index + fftSamplingRate / 2][0])
                ]];
            }

            var fftRect = svg.select('.fft-group')
                .selectAll('.fft-rect')
                .data(fftSelection);
            fftRect.enter().append('rect')
                .classed('fft-rect', true)
                .attr('fill', 'grey')
                .attr('fill-opacity', 0.2)
                .attr('clip-path', 'url(#clip-' + chartID + ')');
            fftRect
                .attr('x', function (d) { return d[0]; })
                .attr('y', 0)
                .attr('height', chartH)
                .attr('width', function (d) { return (d[1] - d[0]); });
            fftRect.exit().remove();

            dispatch.fftResult(fftResult);
        };

        var redrawPaths = function () {
            svg.select('.chart-group')
                .selectAll('.paths')
                .attr('d', line);
        };

        var redraw = function () {

            chartW = width - margin.left - margin.right;
            chartH = height - margin.top - margin.bottom;

            xScale.domain([scale.x0, scale.x1]).range([0, chartW]);
            yScale.domain([scale.y0, scale.y1]).range([chartH, 0]);

            yAxis.tickSize(-chartW, 5);

            svg.attr({ width: width, height: height });

            svg.select('.x-axis-group.axis')
                .attr({ transform: 'translate(0, ' + chartH + ')' })
                .call(xAxis);

            svg.select('.y-axis-group.axis')
                .call(yAxis);

            svg.selectAll('.rect-pane')
                .attr({ width: chartW, height: chartH });

            svg.select('circle.resize')
                .attr({ cx: chartW, cy: chartH });

            // Remove the Y-axis text if multiple scale factors
            if (scaleFactors.length != 1) {
                svg.select('.y-axis-group.axis')
                    .selectAll("text").remove();
            }

            redrawPaths();
            redrawStatistics();
            redrawFftSelection();
        };

        /*
         *  Public methods
         */
        function exports(divContainer) {

            if (!svg) {
                svg = divContainer.select('.chart')
                    .append('svg').attr('class', 'chart');

                var container = svg.append('g').classed('container-group', true)
                    .style('-webkit-user-select', 'none')
                    .attr({ transform: 'translate(' + margin.left + ',' + margin.top + ')' });

                container.append('g').classed('x-axis-group axis', true);
                container.append('g').classed('y-axis-group axis', true);

                container.append('g').classed('statistics-group', true);
                container.append('g').classed('legend-group', true);
                container.append('g').classed('zoom-group', true);
                container.append('g').classed('fft-group', true);

                container.append('g').classed('chart-group', true)
                    .attr('clip-path', 'url(#clip-' + chartID + ')');

                container.append('clipPath').attr('id', 'clip-' + chartID)
                    .append("rect").classed('clip rect-pane', true);

                container.append("rect").classed('overlay rect-pane', true)
                    .attr({ 'fill': 'none', 'pointer-events': 'all' })
                    .on('mousemove', mousemove)
                    .on('mouseout', mouseout)
                    .on('mouseup', mouseup)
                    .on('mousedown', mousedown)
                    .on('dblclick', dblclick)
                    .on('click', click);

                container.append('circle').classed('resize', true)
                    .attr({ 'r': 5, cursor: 'nwse-resize', 'fill': 'none', 'pointer-events': 'all' })
                    .on('mousedown.resize', resize.mousedown);

                // To cash mouseup while we are outside of the chart
                divContainer
                    .on('mouseup', mouseup)
                    .on('mouseup.resize', resize.mouseup)
                    .on('mousemove.resize', resize.mousemove);

                // Scale
                xScale = d3.scale.linear();
                yScale = d3.scale.linear();

                // Axis
                xAxis = d3.svg.axis().scale(xScale).orient('bottom');
                yAxis = d3.svg.axis().scale(yScale).orient('left').tickPadding(8);


                // Return the y-value in px (used with line & area functions)
                var d3yScale = function(_data, _idx) {
                    var len = scaleFactors.length,
                        d1 = function(d) { return d[1]; };

                    return function (d, i) {
                        if (samplingRate === 1) {
                            return yScale(d[1] * scaleFactors[_idx % len]);
                        }
                        if (i % samplingRate == 0) {
                            return yScale(d3.max(_data.slice(i, i + samplingRate), d1) * scaleFactors[_idx % len]);
                        }
                        return yScale(d3.min(_data.slice(i, i + samplingRate), d1) * scaleFactors[_idx % len]);
                    };
                };

                var d3defined = function (d, i) {
                    return d[0] >= (scale.x0 - 0.01) && d[0] <= scale.x1
                        && (i % samplingRate == 0 || (i-1) % samplingRate == 0);
                };

                line = function (_data, _idx) {
                    var d3line = d3.svg.line()
                        .interpolate("linear")
                        .x(function (d) { return xScale(d[0]); })
                        .y(d3yScale(_data, _idx))
                        .defined(d3defined);
                    return d3line(_data);
                };

                area = function (_data, _idx) {
                    var d3area = d3.svg.area()
                        .interpolate("linear")
                        .x(function (d) { return xScale(d[0]); })
                        .y0(function (d) { return yScale(0); })
                        .y1(d3yScale(_data, _idx))
                        .defined(d3defined);
                    return d3area(_data);
                };
            }

            redraw();
            redrawLegend();
            showHidePaths();
        }

        // Add or update '.path'
        exports.addData = function (_data, _class, _overlap) {
            if (_data.length && data.length) {

                // Data length
                var dataLength = data[0].length,
                    newDataLength = _data[0].length;

                // Truncate the data array to remove the overlap number of points
                if (dataLength > _overlap) {
                    dataLength -= _overlap;
                    data.forEach(function (entry) { entry.length = dataLength; });
                }

                // Time of the first new data point to add the data array ...
                var startTime = Math.max(data[0][(dataLength - 1)][0], _data[0][0][0]);

                // Compute the index of the new data array that corresponds to the
                // the first new data point
                var index = _data[0].length;
                while (startTime < _data[0][index - 1][0]) { --index; }

                // Add new point in data array
                data.forEach(function(entry, i) {
                    for (var n = index; n < newDataLength; n++) {
                        entry.push(_data[i][n]);
                    }
                });
            }

            if (_data.length && !data.length) {

                data = _data;
                dataOrig = [];

                // Fast deep  copy...
                data.forEach(function (entry) {
                    var dataTemp = [];
                    entry.forEach(function (entry2) { dataTemp.push([entry2[0], entry2[1]]); });
                    dataOrig.push(dataTemp);
                });
            }

            // Update path with _data
            svg.select('.chart-group')
                .selectAll('.' + _class)
                .data(_data)
                .enter().append("path")
                .attr('stroke-width', 1.5)
                .attr('stroke', pathColors)
                .attr('fill', 'none')
                .attr('opacity', 0.8)
                .attr('d', line)
                .attr('class', function (d, i) { return _class + ' paths x' + i; })
                .style('display', function (d, i) {
                    return (showPath(i) ? null : 'none');
                });

            // Compute scale to fit Bbox!
            if (_class === 'path0') {
                scale = helperService.bbox(dataOrig, scale, param);
            }
        };

        // Remove all '.paths'
        exports.removeData = function () {
            data = [];
            svg.select('.chart-group')
                .selectAll('.paths')
                .remove();

            // Reinitialize the time statistics...
            dispatch.cursorRange({ t0: -1, t1: -1});
            fftResult = null;
            fftLocked = false;
        };

        exports.truncateData = function (_class, overlap) {
            var dataPath = [];
            svg.select('.chart-group')
                .selectAll('.' + _class)
                .data()
                .forEach(function (entry) {
                    dataPath.push(entry.slice(0, -overlap + 1));
                });

            svg.select('.chart-group')
                .selectAll('.' + _class)
                .data(dataPath)
                .attr('d', line);
        };

        exports.width = function (_x) {
            if (!arguments.length) {
                return width;
            }
            width = _x;

            // Resampling rate
            sampleRateCalc();
            return this;
        };

        exports.height = function (_x) {
            if (!arguments.length) {
                return height;
            }
            height = _x;
            return this;
        };

        exports.showPaths = function (_x) {
            if (!arguments.length) {
                return chartPaths;
            }
            chartPaths = _x;
            redrawLegend();
            redrawStatistics();
            showHidePaths();
            return this;
        };

        exports.shiftTimeScale = function (_x) {
            var diff;
            if (_x > scale.x1) {
                while (_x >= scale.x1) {
                    diff = scale.x1 - scale.x0;
                    scale.x0 += diff;
                    scale.x1 += diff;
                }
                return this;
            }
            if (_x < scale.x0) {
                diff = scale.x0 - _x;
                scale.x0 -= diff;
                scale.x1 -= diff;
                return this;
            }
            return this;
        };

        exports.initScale = function (time) {
            scale.x0 = time || 0;
            scale.x1 = scale.x0 + 10;
            scale.y0 = defScale.y0;
            scale.y1 = defScale.y1;
            return this;
        };

        exports.scale = function (_x) {
            if (!arguments.length) {
                return scale;
            }
            var deltaX = scale.x1 - scale.x0;
            scale.x0 = _x.x0;
            scale.x1 = _x.x1;
            scale.y0 = _x.y0 || scale.y0;
            scale.y1 = _x.y1 || scale.y1;

            // In case of x axis scale change, recompute the sample rate ...
            if (scale.x1 - scale.x0 !== deltaX) {
                sampleRateCalc();
            }

            return this;
        };

        exports.mode = function (_x) {
            if (!arguments.length) {
                return chartMode.get();
            }
            chartMode.set(_x);
            if (chartMode.move) {
                svg.select('.overlay').attr({ cursor: 'move' });
            } else {
                svg.select('.overlay').attr({ cursor: 'default' });
            }
            return this;
        };

        exports.resampling = function (_x) {
            sampleRateCalc(_x);
            return samplingRate;
        };

        exports.fftSampleRate = function (_x) {
            if (!arguments.length) {
                return fftSamplingRate;
            }
            fftSamplingRate = _x;

            if (fftSamplingRate === null) {
                fftLocked = false;
            } else {
                dispatch.setMode('stats');
            }
            computeFftSpectrum();
            redrawFftSelection();
            return this;
        };

        exports.smoothingPath = function (_x) {
            if (!arguments.length) {
                return sidePoints;
            }
            smoothingPath((parseInt(_x, 10) - 1) / 2);
            redrawLegend();
            redrawStatistics();
            redrawPaths();
            computeFftSpectrum();
            redrawFftSelection();
            return this;
        };

        exports.redrawLegend = function (_x) {
            cursorPosition = _x;
            redrawLegend();
            return this;
        };

        exports.redrawStatistics = function (_x) {
            cursorRange = _x;
            redrawStatistics();
            return this;
        };

        exports.clean = function () {
            dataOrig = [];
            data = [];
            svg.select('.chart-group')
                .selectAll('.paths')
                .remove();
        };

        d3.rebind(exports, dispatch, 'on');
        return exports;
    };

    return chart;
});

angular.module('IOLabApp').factory('paramPlotService',
    function (helperService, configService, selectionService, chartModeService) {
    'use strict';

    var paramPlot = function (args) {

        var remote = args.remote,
            config = args.config,
            sensor1 = args.sensor1.sensorType,
            sensor2 = args.sensor2.sensorType,
            chartID = remote + '-' + config + '-' + sensor1 + '-' + sensor1,
            sensorLen = {
                1: args.sensor1.sampleSize,
                2: args.sensor1.sampleSize
            },
            sensorCoord = {
                1: -1,
                2: -1
            },
            param = {
                1: configService.getSensor(sensor1) || {},
                2: configService.getSensor(sensor2) || {}
            };

        // Default width and height (setter/getter)
        var width = 560, height = 370;

        // Default margin
        var margin = { top: 25, right: 25, bottom: 45, left: 50 };

        // Chart scales' range
        var defScale = {};
        defScale.x0 = param[1].scales[0] || -20;
        defScale.x1 = param[1].scales[1] || 20;
        defScale.y0 = param[2].scales[0] || -20;
        defScale.y1 = param[2].scales[1] || 20;
        var scale = Object.create(defScale);

        var d3Mouse, d3MouseDown;

        // Variable containing the data to bind. Used also to get the
        // value of t, x1, x2, ... when hovering
        var data = [], dataOrig = [], timeRange, timeRanges, showTimeScale = false;

        // D3 object for the chart rendering
        var svg, chartW, chartH, brushH,
            xScale, yScale, y2Scale, tScale,
            xAxis, yAxis, tAxis,
            line, line2,
            brush;

        /*
         * Cursor selection object
         */
        var selection = selectionService();

        /*
         * Chart Mode object
         */
        var chartMode = chartModeService();

        var dispatch = d3.dispatch(
            'cursorPosition',
            'cursorRange',
            'scaleChanges',
            'setMode'
        );

        var brushed = function() {
            timeRanges = brush.empty() ? null : brush.extent();
            svg.select('g.chart-group')
                .selectAll('path.paths')
                .attr('d', line);
        };

        var resize = {
            active: false,
            d3MouseDown: null,
            d3MouseElement: null
        };
        resize.mousedown = function () {
            resize.active = true;
            resize.d3MouseElement = this;
            resize.d3MouseDown = d3.mouse(this);
        };
        resize.mousemove = function () {
            if (!resize.active) {
                return;
            }
            d3Mouse = d3.mouse(resize.d3MouseElement);
            width += d3Mouse[0] - resize.d3MouseDown[0];
            height += d3Mouse[1] - resize.d3MouseDown[1];
            resize.d3MouseDown = d3Mouse;
            redraw();
        };
        resize.mouseup = function () {
            resize.active = false;
            resize.d3MouseDown = null;
        };

        var mousedown = function () {
            // Needed to check the click at the same position
            d3MouseDown = d3.mouse(this);

            if (selection.p0.locked) {
                selection.reset();
                if (chartMode.zoom) {
                    redrawZoomSelection();
                }
            }

            if (chartMode.stats || chartMode.zoom || chartMode.move) {
                // Keep a copy of the scale
                selection.scale = angular.copy(scale);

                // Lock p0 and keep the value
                selection.p0.locked = true;
                selection.p0.x = xScale.invert(d3MouseDown[0]);
                selection.p0.y = yScale.invert(d3MouseDown[1]);
            }
        };

        var mousemove = function () {
            // Index of the data point corresponding to the mouse position
            d3Mouse = d3.mouse(this);

            // Selection locked (update the selection)
            if (selection.p0.locked) {

                selection.p1.x = xScale.invert(d3Mouse[0]);
                selection.p1.y = yScale.invert(d3Mouse[1]);

                if (chartMode.move) {
                    scale.x0 -= (selection.p1.x - selection.p0.x);
                    scale.x1 -= (selection.p1.x - selection.p0.x);
                    scale.y0 -= (selection.p1.y - selection.p0.y);
                    scale.y1 -= (selection.p1.y - selection.p0.y);
                    redraw();
                }
            }

            if (chartMode.zoom) {
                redrawZoomSelection();
            }
        };

        var mouseout = function () {
            // Erase the cross ...
            d3Mouse = null;
            redrawZoomSelection();
        };

        var mouseup = function () {
            // Set the new scale in zoom mode
            if (chartMode.zoom && selection.notNull()) {
                // Keep the old scale for further use
                chartMode.scales.push(angular.copy(scale));

                scale.x0 = Math.min(selection.p0.x, selection.p1.x);
                scale.x1 = Math.max(selection.p0.x, selection.p1.x);
                scale.y0 = Math.min(selection.p0.y, selection.p1.y);
                scale.y1 = Math.max(selection.p0.y, selection.p1.y);
                selection.reset();
                redraw();
                redrawZoomSelection();
            }
            selection.reset();
        };

        var click = function () {
            var scalesLen = chartMode.scales.length;
            if (chartMode.zoom && !!scalesLen && d3MouseDown[0] === d3.mouse(this)[0]) {
                scale = chartMode.scales.splice(-1, 1)[0];
                redraw();
                redrawZoomSelection();
            }
        };

        var dblclick = function () {
            // Reset the zoom scale to the bbox
            if (chartMode.zoom) {
                bbox();
                chartMode.scales = [];
                redraw();
            }
        };

        var redrawZoomSelection = function () {

            /*
             * Cross
             */
            var crossData = chartMode.zoom && d3Mouse? [d3Mouse[0], d3Mouse[1]] : [],
                crossGroup;

            crossGroup = svg.select('.zoom-group')
                .selectAll('.zoom-cross')
                .data(crossData)  // data = [y]
                .attr('x1', function (d, i) { return i ? 1 : d; })
                .attr('x2', function (d, i) { return i ? chartW : d; })
                .attr('y2', function (d, i) { return i ? d : chartH; })
                .attr('y1', function (d, i) { return i ? d : 0; });
            crossGroup.enter()
                .append('line')
                .attr('class', 'zoom-cross')
                .attr({'stroke-width': 1, 'stroke': '#555555'});
            crossGroup.exit().remove();


            /*
             * Rectangle
             */
            var zoomData, zoomGroup;

            if (selection.p0.x !== -1 && selection.p1.x !== -1) {
                zoomData = [{
                    x0: selection.p0.x < selection.p1.x ? selection.p0.x : selection.p1.x,
                    x1: selection.p0.x < selection.p1.x ? selection.p1.x : selection.p0.x,
                    y0: selection.p0.y < selection.p1.y ? selection.p0.y : selection.p1.y,
                    y1: selection.p0.y < selection.p1.y ? selection.p1.y : selection.p0.y
                }];
            } else {
                zoomData = [];
            }

            zoomGroup = svg.select('.zoom-group')
                .selectAll('.zoom-rect')
                .data(zoomData)
                .attr('x', function (d) { return xScale(d.x0); })
                .attr('y', function (d) { return yScale(d.y1); })
                .attr('width', function (d) { return xScale(d.x1) - xScale(d.x0); })
                .attr('height', function (d) { return yScale(d.y0) - yScale(d.y1); });
            zoomGroup.enter()
                .append('rect')
                .attr('class', 'zoom-rect')
                .style({fill: 'none', stroke: '#555555', 'stroke-width': 1.5, 'stroke-dasharray': '6,2'});
            zoomGroup.exit().remove();
        };

        var pathColors = function (d, i) {
            if (param.pathColors) {
                return param.pathColors[i];
            }
            return configService.getDefaultColor(i);
        };

        var redrawPaths = function () {

            svg.select('g.chart-group')
                .selectAll('path.paths')
                .attr('d', line);

            if (showTimeScale) {
                svg.select('g.time-scale-group')
                    .selectAll('path.time-paths')
                    .attr('d', line2);
            }
        };

        var redraw = function () {

            chartW = width - margin.left - margin.right;

            if (showTimeScale) {
                brushH = 80;
                chartH = height - margin.top - margin.bottom - brushH;

                tScale.domain([data[0][0], data.slice(-1)[0][0]]).range([0, chartW]);
                y2Scale.domain([0, 1]).range([chartH + brushH, chartH + margin.bottom * 1.2])

                svg.select('g.t-axis-group.axis')
                    .attr({ transform: 'translate(0, ' + (chartH + brushH) + ')' })
                    .call(tAxis);

                svg.select('g.t.brush')
                    .attr({ transform: 'translate(0, ' + (chartH + margin.bottom * 1.2) + ')' })
                    .call(brush);

                svg.select('g.t.brush')
                    .selectAll('rect')
                    .attr('y', -5)
                    .attr('height', brushH - margin.bottom * 1.2 + 4);

                svg.select('g.t.brush')
                    .selectAll('g.resize')
                    .selectAll('rect')
                    .attr('y', 5)
                    .attr('height', brushH - margin.bottom * 1.2 - 6);

            } else {
                brushH = 0;
                chartH = height - margin.top - margin.bottom;
            }

            xScale.domain([scale.x0, scale.x1]).range([0, chartW]);
            yScale.domain([scale.y0, scale.y1]).range([chartH, 0]);

            xAxis.tickSize(-chartH, 5);
            yAxis.tickSize(-chartW, 5);

            svg.attr({ width: width, height: height });

            svg.select('g.x-axis-group.axis')
                .attr({ transform: 'translate(0, ' + chartH + ')' })
                .call(xAxis);
            svg.select('g.x-axis-legend')
                .attr({ transform: 'translate(' + chartW / 2 + ', 30)' });

            svg.select('g.y-axis-group.axis')
                .call(yAxis);
            svg.select('g.y-axis-legend')
                .attr({ transform: 'translate(-40 , ' + chartH / 2 + ') rotate(270)' });

            svg.selectAll('.rect-pane')
                .attr({ width: chartW, height: chartH });

            svg.select('circle.resize')
                .attr({ cx: chartW, cy: chartH });

            redrawPaths();
        };

        var coord = function (axis, d) {
            // Norm of the vector !
            if (sensorCoord[axis] == -1) {
                var i, len = sensorLen[axis], norm2 = 0,
                    start = axis == 1 ? 1 : (1 + sensorLen[1]);
                for (i = start; i < (len + start); i++) {
                    norm2 +=  d[i] * d[i];
                }
                return Math.sqrt(norm2);
            }

            // Specific coordinate
            var idx = 1 + sensorCoord[axis];
            if (axis == 2) { idx += sensorLen[1]; }
            return d[idx];
        };

        var bbox = function () {

            var dataArray, delta, min, max;

            // Sensor 1
            dataArray = data.map(function (d) { return coord(1, d) });
            max = Math.max.apply(Math, dataArray);
            min = Math.min.apply(Math, dataArray);
            delta = max - min || 10;

            scale.x0 = min - delta / 4;
            scale.x1 = max + delta / 4;

            // Sensor 2
            dataArray = data.map(function (d) { return coord(2, d) });
            max = Math.max.apply(Math, dataArray);
            min = Math.min.apply(Math, dataArray);
            delta = max - min || 10;

            scale.y0 = min - delta / 4;
            scale.y1 = max + delta / 4;
        };

        /*
         *  Public methods
         */
        function exports(divContainer) {

            if (!svg) {
                svg = divContainer.select('.chart')
                    .append('svg').attr('class', 'chart');

                var container = svg.append('g').classed('container-group', true)
                    .style('-webkit-user-select', 'none')
                    .attr({ transform: 'translate(' + margin.left + ',' + margin.top + ')' });

                container.append('g').classed('x-axis-group axis', true)
                    .append('g').attr('class', 'x-axis-legend')
                    .append('text').text(param[1].desc + ' (' + param[1].units[0]  + ')')
                    .attr({'text-anchor': 'middle', 'alignment-baseline': 'middle', fill: pathColors(null, 0)})
                    .style({'font-size': 14});

                container.append('g').classed('y-axis-group axis', true)
                    .append('g').attr('class', 'y-axis-legend')
                    .append('text').text(param[2].desc + ' (' + param[2].units[0]  + ')')
                    .attr({'text-anchor': 'middle', 'alignment-baseline': 'middle', fill: pathColors(null, 1)})
                    .style({'font-size': 14});

                container.append('g').classed('zoom-group', true);

                container.append('g').classed('chart-group', true)
                    .attr('clip-path', 'url(#clip-' + chartID + ')');

                var timeScaleContainer = container
                    .append('g').attr('class', 'time-scale-group');
                timeScaleContainer.append('g').attr('class', 't-paths-group');
                timeScaleContainer.append('g').attr('class', 't-axis-group axis');
                timeScaleContainer.append('g').attr('class', 't brush');

                container.append('clipPath').attr('id', 'clip-' + chartID)
                    .append("rect").classed('clip rect-pane', true);

                container.append("rect").classed('overlay rect-pane', true)
                    .attr({ 'fill': 'none', 'pointer-events': 'all' })
                    .on('mousemove', mousemove)
                    .on('mouseout', mouseout)
                    .on('mouseup', mouseup)
                    .on('mousedown', mousedown)
                    .on('dblclick', dblclick)
                    .on('click', click);

                container.append('circle').classed('resize', true)
                    .attr({ 'r': 5, cursor: 'nwse-resize', 'fill': 'none', 'pointer-events': 'all' })
                    .on('mousedown.resize', resize.mousedown);

                // To cash mouseup while we are outside of the chart
                divContainer
                    .on('mouseup.resize', resize.mouseup)
                    .on('mousemove.resize', resize.mousemove);

                // Scale
                xScale = d3.scale.linear();
                yScale = d3.scale.linear();
                tScale = d3.scale.linear();
                y2Scale = d3.scale.linear();

                // Axis
                xAxis = d3.svg.axis().scale(xScale).orient('bottom').tickPadding(8);
                yAxis = d3.svg.axis().scale(yScale).orient('left').tickPadding(8);
                tAxis = d3.svg.axis().scale(tScale).orient('bottom').tickPadding(8);

                // brush
                brush = d3.svg.brush()
                    .x(tScale)
                    .on("brush", brushed);

                var dataDefined = function (_data) {
                    var len = _data.length,
                        timeEnd = _data[len-1][0];

                    return function (d, i) {
                        if (showTimeScale && timeRanges) {
                            return timeRanges[0] <= i / len * timeEnd &&  i / len * timeEnd <= timeRanges[1];
                        }
                        return i >= len - timeRange / 10;
                    }
                };

                line = function (_data, _idx) {
                    var d3line = d3.svg.line()
                        .interpolate("linear")
                        .x(function (d) { return xScale(coord(1, d)); })
                        .y(function (d) { return yScale(coord(2, d)); })
                        .defined(dataDefined(_data));
                    return d3line(_data);
                };

                line2 = function (_data, _idx) {
                    var dataArray = _data.map(function (d) { return coord(_idx + 1, d) }),
                        max = Math.max.apply(Math, dataArray),
                        min = Math.min.apply(Math, dataArray),

                        d3line = d3.svg.line()
                        .interpolate("linear")
                        .x(function (d) { return tScale(d[0]); })
                        .y(function (d) {
                           return y2Scale((coord(_idx + 1, d) - min) / (max - min));
                        });
                    return d3line(_data);
                };
            }

            redraw();
        }

        // Add or update '.path'
        exports.addData = function (_data) {

            if (data.length && _data.length) {

                // Data length
                var dataLength = data.length,
                    newDataLength = _data.length;

                // Time of the first new data point to add the data array ...
                var startTime = Math.max(data[(dataLength - 1)][0], _data[0][0]);

                // Compute the index of the new data array that corresponds to the
                // the first new data point
                var index = _data.length;
                while (startTime < _data[index - 1][0]) { --index; }

                // Add new point in data array
                for (var n = index; n < newDataLength; n++) {
                    data.push(_data[n]);
                }
            }

            if (!data.length && _data.length) {

                data = _data;
                dataOrig = [];

                // Fast deep  copy...
                data.forEach(function (entry) {
                    dataOrig.push(entry.map(function(d) { return d; }));
                });
            }

            svg.select('g.chart-group')
                .selectAll('path.paths')
                  .data([data])
                  .attr('d', line)
                .enter().append("path")
                  .attr('stroke-width', 1.5)
                  .attr('stroke', pathColors)
                  .attr('fill', 'none')
                  .attr('opacity', 0.8)
                  .attr('class', 'paths');
        };

        // Remove all '.paths'
        exports.removeData = function () {
            data = [];
            svg.select('path.chart-group')
                .selectAll('.paths')
                .remove();
        };

        exports.width = function (_x) {
            if (!arguments.length) {
                return width;
            }
            width = _x;
            return this;
        };

        exports.height = function (_x) {
            if (!arguments.length) {
                return height;
            }
            height = _x;
            return this;
        };

        exports.initScale = function (time) {
            scale.x0 = defScale.x0;
            scale.x1 = defScale.x1;
            scale.y0 = defScale.y0;
            scale.y1 = defScale.y1;
            return this;
        };

        exports.scale = function (_x) {
            if (!arguments.length) {
                return scale;
            }
            scale.x0 = _x.x0;
            scale.x1 = _x.x1;
            scale.y0 = _x.y0;
            scale.y1 = _x.y1;
            return this;
        };

        exports.mode = function (_x) {
            if (!arguments.length) {
                return chartMode.get();
            }
            chartMode.set(_x);
            if (chartMode.move) {
                svg.select('.overlay').attr({ cursor: 'move' });
            } else {
                svg.select('.overlay').attr({ cursor: 'default' });
            }
            return this;
        };

        exports.coordinate = function(idx, coord) {
            if (!arguments.length) {
                return sensorCoord[idx] ;
            }
            sensorCoord[idx] = coord;

            // Redraw when modifying the coordinate type...
            if (showTimeScale) {
                bbox();
                redraw();
            }
        };

        exports.timeRange = function(_x) {
            if (!arguments.length) {
                return timeRange;
            }
            timeRange = _x;
            return this;
        }

        exports.showTimeScale = function(_x) {
            if (!arguments.length) {
                return showTimeScale;
            }
            showTimeScale = _x;

            svg.select('g.time-scale-group')
                .attr('display', showTimeScale ? null : 'none');

            svg.select('g.t-paths-group')
                .selectAll('path.time-paths')
                  .data(showTimeScale ? [data, data] : [])
                  .attr('d', line2)
                .enter().append("path")
                  .attr('stroke-width', 1.5)
                  .attr('stroke', pathColors)
                  .attr('fill', 'none')
                  .attr('opacity', 0.8)
                  .attr('class', 'time-paths');

            // Compute bbox ...
            if (showTimeScale) {
                bbox();
            } else {
                brush.clear();
                timeRanges = null;
                scale = angular.copy(defScale);
            }

            redraw();
            return this;
        };

        exports.clean = function () {
            dataOrig = [];
            data = [];
            svg.select('.chart-group')
                .selectAll('.paths')
                .remove();
        };

        d3.rebind(exports, dispatch, 'on');
        return exports;
    };

    return paramPlot;
});

angular.module('IOLabApp').factory('fftService', function (helperService, configService) {
    'use strict';

    var fft = function (_data) {

        var remote = _data.remote,
            config = _data.config,
            sensor = _data.sensorType,
            fftID = remote + '-' + config + '-' + sensor,
            sensorParam = configService.getSensor(sensor) || {};

        // Default width and height (setter/getter)
        var width = 550, height = 120;

        // Default margin
        var margin = { top: 10, right: 20, bottom: 20, left: 30 };

        // Chart paths (used to show/hide paths)
        var chartPaths = [];

        // Variable containing the data to bind. Used also to get the
        // value of t, x1, x2, ... when hovering
        var frequency, samplingRate, spectrums, maxSpectrum;

        // D3 object for the chart rendering
        var svg, chartOrig, chartW, chartH, line, xScale, yScale, xAxis, yAxis;

        //var dispatch = d3.dispatch('cursorPosition', 'cursorRange', 'fftResult');
        var drag = d3.behavior.drag();

        var mousemove = function () {
            redrawLegend();
        };

        var mouseout = function () {
            svg.select('.legend-group').style('display', 'none');
        };

        var zoom = d3.behavior.zoom()
            .on('zoom', function () {
                chartOrig = -1 * d3.event.translate[0];
                redrawFft();
            });

        var showPath = function (i) {
            return chartPaths.length == 0 || (chartPaths[i] && chartPaths[i].show);
        };

        // Build Y position:
        var yTextPosition = function (d, i) {
            var yPosition, n;
            for (yPosition = 0, n = 1; n <= i; n++) {
                if (showPath(n - 1)) {
                    yPosition += 13;
                }
            }
            return yPosition;
        };

        var legendText = function (d, i) {
            if (i === 0) {
                return 'ν: ' + d.toFixed(3) + ' Hz';
            }
            if (!showPath(i - 1)) {
                return '';
            }
            return '|A|: ' + d.toPrecision(3);
        };

        var redrawLegend = function () {
            // Index of the data point corresponding to the mouse
            // position from the controller
            var index = helperService.getDataIndex(
                spectrums,
                xScale.invert(d3.mouse(svg.select('.fft-group').node())[0])
            );

            if (!spectrums) {
                return;
            }

            // Build data array to render [t, x0, x1, ...]
            var svgData = [spectrums[0][index][0]];
            // x0, x1, ...
            spectrums.forEach(function (entry) { return svgData.push(entry[index][1]); });

            // Activate the display of the legend
            svg.select('.legend-group').style('display', null);

            // Update/create the sensor values
            svg.select('.legend-group')
                .selectAll('.legend-text')
                  .data(svgData)
                  .attr('x', 2 * chartW / 3)
                  .attr('y', yTextPosition)
                  .text(legendText)
                .enter().append('text')
                  .classed('legend-text', true)
                  .attr('fill', legendColors);

            // Update/create a line for the time position
            svg.select('.legend-group')
                .selectAll('.legend-line')
                  .data([svgData[0]])  // data = [t]
                  .attr('x1', function (d) { return xScale(d); })
                  .attr('x2', function (d) { return xScale(d); })
                  .attr('y2', chartH)
                  .attr('y1', 0)
                .enter().append('line')
                  .classed('legend-line', true)
                  .attr('stroke-width', 1)
                  .attr('stroke', '#555555');
        };

        var legendColors = function (d, i) {
            if (i === 0) {
                return '#444444';
            }
            return pathColors(d, i - 1);
        };

        var pathColors = function (d, i) {
            if (sensorParam.pathColors) {
                return sensorParam.pathColors[i];
            }
            return configService.getDefaultColor(i);
        };

        var resetSVG = function () {

            svg.attr({ width: 0, height: 0 });
            svg.select('.fft-group')
                .selectAll('.fft-path')
                .data([])
                .exit().remove();
        };

        var initSVG = function () {

            if (samplingRate === null) {
                resetSVG();
                return;
            }

            chartOrig = 0;
            chartW = width - margin.left - margin.right;
            chartH = height - margin.top - margin.bottom;

            svg.attr({ width: width, height: height });

            svg.selectAll('.rect-pane')
                .attr({ width: chartW, height: chartH });

            // Show default scale ..
            var scale = configService.getSampleRate(sensor, config) / samplingRate / 4;

            xScale.domain([0, scale * chartW]).range([0, chartW]);
            yScale.domain([0, 1]).range([chartH, 0]);

            svg.select('.x-axis-group.axis')
                .attr({ transform: 'translate(0, ' + chartH + ')' })
                .call(xAxis);

            svg.select('.y-axis-group.axis')
                .call(yAxis);
        };

        var redrawFft = function () {

            if (samplingRate === null) {
                return;
            }

            var scale = frequency / samplingRate / 4;

            xScale.domain([scale * chartOrig, scale * (chartW + chartOrig)]).range([0, chartW]);
            yScale.domain([0, maxSpectrum]).range([chartH, 0]);

            line.interpolate('step-after')
                .x(function (d) { return xScale(d[0]) - 2; })
                .y(function (d) { return yScale(d[1]); });

            svg.select('.x-axis-group.axis')
                .attr({ transform: 'translate(0, ' + chartH + ')' })
                .call(xAxis);

            svg.select('.y-axis-group.axis')
                .call(yAxis);

            var fftLines = svg
                .select('.fft-group')
                .selectAll('.fft-path')
                .data(spectrums);
            fftLines.enter()
                .append('path')
                .attr('class', 'fft-path')
                .attr('fill', 'none')
                .attr('stroke-width', 1)
                .attr('stroke', pathColors);
            fftLines
                .attr('d', line)
                .style('display', function (d, i) {
                    return (showPath(i) ? null : 'none');
                });
            fftLines.exit().remove();
        };

        function exports(divContainer) {

            if (!svg) {
                svg = divContainer.select('.fft')
                    .append('svg').attr('class', 'fft');

                var container = svg.append('g').classed('container-group', true)
                    .style('-webkit-user-select', 'none')
                    .attr({ transform: 'translate(' + margin.left + ',' + margin.top + ')' });

                container.append('g').classed('x-axis-group axis', true);
                container.append('g').classed('y-axis-group axis', true);
                container.append('g').classed('legend-group', true);

                container.append('g').classed('fft-group', true)
                    .attr('clip-path', 'url(#clip-fft-' + fftID + ')');

                container.append('clipPath').attr('id', 'clip-fft-' + fftID)
                    .append("rect").classed('clip rect-pane', true);

                container.append("rect").classed('overlay rect-pane', true)
                    .attr({ 'fill': 'none', 'pointer-events': 'all' })
                    .on('mousemove', mousemove)
                    .on('mouseout', mouseout)
                    .call(zoom);

                // d3 Scale
                xScale = d3.scale.linear();
                yScale = d3.scale.linear();

                // d3 line
                line = d3.svg.line();

                // d3 Axis
                xAxis = d3.svg.axis().scale(xScale).orient('bottom');
                yAxis = d3.svg.axis().scale(yScale).orient('left');
            }
        }

        // Add or update '.path'
        exports.redraw = function (_data) {
            var maxSpectrums,
                bisect,
                index = 1;

            // Init drawing variables
            if (_data === null) {
                return;
            }

            frequency = _data.frequency;
            spectrums = _data.spectrums;
            samplingRate = _data.samplingRate;

            // Discard bins with frequency < to fftThreshold
            // to compute the max spectrum (max 'y' scale)
            if (sensorParam.fftThreshold) {
                bisect = d3.bisector(function (d) { return d[0]; }).right;
                index = bisect(spectrums[0], sensorParam.fftThreshold);
            }

            // Compute the max value for the spectrum (max 'y' scale)
            maxSpectrums = spectrums.map(function (spectrum, idx) {
                if (!showPath(idx)) {
                    return 0;
                }
                return Math.max.apply(null, spectrum.slice(index).map(function (item) {
                    return item[1];
                }));
            });

            maxSpectrum = Math.max.apply(null, maxSpectrums);

            redrawFft();
        };

        exports.samplingRate = function (_x) {
            if (!arguments.length) {
                return samplingRate;
            }
            samplingRate = _x;
            initSVG();
            return this;
        };

        exports.width = function (_x) {
            if (!arguments.length) {
                return width;
            }
            width = _x;
            initSVG();
            redrawFft();
            return this;
        };

        exports.height = function (_x) {
            if (!arguments.length) {
                return height;
            }
            height = _x / 2;
            initSVG();
            redrawFft();
            return this;
        };

        exports.showPaths = function (_x) {
            if (!arguments.length) {
                return chartPaths;
            }
            chartPaths = _x;
            redrawFft();
            return this;
        };

        //d3.rebind(exports, dispatch, 'on');
        return exports;
    };

    return fft;
});

angular.module('IOLabApp').factory('gaugeService', function () {
    'use strict';

    var gauge = function (placeholderName, configuration) {

        var exports = {},
            config = {},
            body;

        function configure(configuration) {
            config = configuration;

            config.size = config.size * 0.9;

            config.raduis = config.size * 0.97 / 2;
            config.cx = config.size / 2;
            config.cy = config.size / 2;

            config.min = undefined !== configuration.min ? configuration.min : -20;
            config.max = undefined !== configuration.max ? configuration.max :  20;
            config.range = config.max - config.min;

            config.majorTicks = configuration.majorTicks || 5;
            config.minorTicks = configuration.minorTicks || 2;

            config.transitionDuration = configuration.transitionDuration || 250;
        }

        function valueToDegrees(value) {
            return value / config.range * 270 - (config.min / config.range * 270 + 45);
        }

        function valueToRadians(value) {
            return valueToDegrees(value) * Math.PI / 180;
        }

        function valueToPoint(value, factor) {
            return { x: config.cx - config.raduis * factor * Math.cos(valueToRadians(value)),
                y: config.cy - config.raduis * factor * Math.sin(valueToRadians(value)) };
        }

        function buildPointerPath(value) {
            function valueToPoinCenter(value, factor) {
                var point = valueToPoint(value, factor);
                point.x -= config.cx;
                point.y -= config.cy;
                return point;
            }

            var delta = config.range / 13;
            var head = valueToPoinCenter(value, 0.85);
            var head1 = valueToPoinCenter(value - delta, 0.12);
            var head2 = valueToPoinCenter(value + delta, 0.12);

            var tailValue = value - (config.range * (1 / (270 / 360)) / 2);
            var tail = valueToPoinCenter(tailValue, 0.28);
            var tail1 = valueToPoinCenter(tailValue - delta, 0.12);
            var tail2 = valueToPoinCenter(tailValue + delta, 0.12);

            return [head, head1, tail2, tail, tail1, head2, head];
        }

        exports.render = function (element) {
            var fontSize;
            var major, minor, majorDelta, minorDelta;
            var point, point1, point2;

            body = d3.select(element)
                .select("#" + placeholderName)
                .append("svg:svg")
                .attr("class", "gauge")
                .attr("width", config.size)
                .attr("height", config.size);

            body.append("svg:circle")
                .attr("cx", config.cx)
                .attr("cy", config.cy)
                .attr("r", config.raduis)
                .style("fill", "#ccc")
                .style("stroke", "#000")
                .style("stroke-width", "0.5px");

            body.append("svg:circle")
                .attr("cx", config.cx)
                .attr("cy", config.cy)
                .attr("r", 0.9 * config.raduis)
                .style("fill", "#fff")
                .style("stroke", "#e0e0e0")
                .style("stroke-width", "2px");

            if (config.label !== undefined) {
                fontSize = Math.round(config.size / 9);
                body.append("svg:text")
                    .attr("x", config.cx)
                    .attr("y", config.cy / 2 + fontSize / 2)
                    .attr("dy", fontSize / 2)
                    .attr("text-anchor", "middle")
                    .text(config.label)
                    .style("font-size", fontSize + "px")
                    .style("fill", "#333")
                    .style("stroke-width", "0px");
            }

            fontSize = Math.round(config.size / 16);
            majorDelta = config.range / (config.majorTicks - 1);

            for (major = config.min; major <= config.max; major += majorDelta) {
                minorDelta = majorDelta / config.minorTicks;
                for (minor = major + minorDelta; minor < Math.min(major + majorDelta, config.max); minor += minorDelta) {
                    point1 = valueToPoint(minor, 0.75);
                    point2 = valueToPoint(minor, 0.85);

                    body.append("svg:line")
                        .attr("x1", point1.x)
                        .attr("y1", point1.y)
                        .attr("x2", point2.x)
                        .attr("y2", point2.y)
                        .style("stroke", "#666")
                        .style("stroke-width", "1px");
                }

                point1 = valueToPoint(major, 0.7);
                point2 = valueToPoint(major, 0.85);

                body.append("svg:line")
                    .attr("x1", point1.x)
                    .attr("y1", point1.y)
                    .attr("x2", point2.x)
                    .attr("y2", point2.y)
                    .style("stroke", "#333")
                    .style("stroke-width", "2px");

                if (major === config.min || major === config.max) {
                    point = valueToPoint(major, 0.63);

                    body.append("svg:text")
                        .attr("x", point.x)
                        .attr("y", point.y)
                        .attr("dy", fontSize / 3)
                        .attr("text-anchor", major == config.min ? "start" : "end")
                        .text(major)
                        .style("font-size", fontSize + "px")
                        .style("fill", "#333")
                        .style("stroke-width", "0px");
                }
            }

            var pointerContainer = body.append("svg:g").attr("class", "pointerContainer");
            var midValue = (config.min + config.max) / 2;
            var pointerPath = buildPointerPath(midValue);

            var pointerLine = d3.svg.line()
                .x(function (d) { return d.x; })
                .y(function (d) { return d.y; })
                .interpolate("basis");

            pointerContainer.selectAll("path")
                .data([pointerPath])
                .enter()
                .append("svg:path")
                .attr("d", pointerLine)
                .style("fill", "#dc3912")
                .style("stroke", "#c63310")
                .style("fill-opacity", 0.7);

            pointerContainer.append("svg:circle")
                .attr("cx", config.cx)
                .attr("cy", config.cy)
                .attr("r", 0.12 * config.raduis)
                .style("fill", "#4684EE")
                .style("stroke", "#666")
                .style("opacity", 1);

            fontSize = Math.round(config.size / 10);
            pointerContainer.selectAll("text")
                .data([midValue])
                .enter()
                .append("svg:text")
                .attr("x", config.cx)
                .attr("y", config.size - config.cy / 4 - fontSize)
                .attr("dy", fontSize / 2)
                .attr("text-anchor", "middle")
                .style("font-size", fontSize + "px")
                .style("fill", "#000")
                .style("stroke-width", "0px");

            exports.redraw(0, 0);
        };

        exports.redraw = function (value, transitionDuration) {
            var _currentRotation;

            var pointerContainer = body.select(".pointerContainer");
            pointerContainer.selectAll("text").text(value.toFixed(1));

            var pointer = pointerContainer.selectAll("path");
            pointer.transition()
                .duration(undefined !== transitionDuration ? transitionDuration : config.transitionDuration)
                .attrTween("transform", function () {
                    var pointerValue = value;
                    if (value > config.max) {
                        pointerValue = config.max + 0.02 * config.range;
                    } else if (value < config.min) {
                        pointerValue = config.min - 0.02 * config.range;
                    }
                    var targetRotation = (valueToDegrees(pointerValue) - 90);
                    var currentRotation = _currentRotation || targetRotation;
                    _currentRotation = targetRotation;

                    return function (step) {
                        var rotation = currentRotation + (targetRotation - currentRotation) * step;
                        return "translate(" + config.cx + ", " + config.cy + ") rotate(" + rotation + ")";
                    };
                });
        };

        // initialization
        configure(configuration);

        return exports;
    };

    return gauge;
});

angular.module('IOLabApp').service('configService', function () {
    'use strict';

    var self = this;

    var colorRed = '#BB0000';
    var colorBlue = '#0000BB';
    var colorGreen = '#008800';
    var colorBlack = '#000000';
    var colorPurle = '#C431DD';
    var colorOrange = '#FF9A24';

    var dongleModes = {};
    dongleModes[1] = {desc: 'IDLE'};
    dongleModes[2] = {desc: 'PAIRING'};
    dongleModes[4] = {desc: 'PAIRED'};
    dongleModes[8] = {desc: 'CONNECTED'};
    dongleModes[16] = {desc: 'DATA'};

    var sensors = {};
    sensors[1] = { code: 1, desc: 'Accelerometer', shortDesc: 'Accel',
                    pathColors: [colorRed, colorBlue, colorGreen ],
                    scales: [-20, 20],
                    legends: ['Ax', 'Ay', 'Az'],
                    units: ['m/s²', 'm/s²', 'm/s²']
        };

    sensors[2] = { code: 2, desc: 'Magnetometer', shortDesc: 'Magn',
                    pathColors: [colorRed, colorBlue, colorGreen ],
                    scales: [-75, 75],
                    legends: ['Bx', 'By', 'Bz'],
                    units: ['µT', 'µT', 'µT']
        };

    sensors[3] = { code: 3, desc: 'Gyroscope', shortDesc: 'Gyro',
                    pathColors: [colorRed, colorBlue, colorGreen ],
                    scales: [-20, 20],
                    legends: ['Ωx', 'Ωy', 'Ωz'],
                    units: ['rad/s', 'rad/s', 'rad/s']
        };

    sensors[4] = { code: 4, desc: 'Barometer', shortDesc: 'Baro',
                    scales: [0, 120],
                    legends: ['Pressure'],
                    units: ['kPa'],
                    fixedScaleY: true
        };

    sensors[6] = { code: 6, desc: 'Microphone', shortDesc: 'Micro',
                    scales: [0, 10],
                    fftThreshold: 25 // Hz
        };

    sensors[7] = { code: 7, desc: 'Light', shortDesc: 'Light',
                    scales: [0, 10]
        };

    sensors[8] = { code: 8, desc: 'Force', shortDesc: 'Force',
                    pathColors: [colorBlue],
                    scales: [-5, 5],
                    legends: ['Fy'],
                    units: ['N']
        };

    sensors[9] = { code: 9, desc: 'Wheel', shortDesc: 'Wheel',
                    pathColors: [colorRed, colorGreen, colorBlue],
                    scaleFactors: [2, 1.0, 0.2],
                    scales: [-5, 5],
                    legends: ['Ry', 'Vy', 'Ay'],
                    units: ['m', 'm/s', 'm/s²']
        };

    sensors[10] = { code: 10, desc: 'ECG (requires plugin)', shortDesc: 'ECG',
                    pathColors: [colorRed, colorBlue, colorGreen],
                    legends: ['V1', 'V2', 'V3'],
                    units: ['mV', 'mV', 'mV']
        }; // 0x0A

    sensors[11] = { code: 11, desc: 'Battery', shortDesc: 'Bat',
                    scales: [0, 4],
                    legends: ['Battery'],
                    units: ['V'],
                    fixedScaleY: true
        }; // 0x0B

    sensors[12] = { code: 12, desc: 'High Gain', shortDesc: 'HG',
                    scales: [-1, 1],
                    legends: ['Voltage'],
                    units: ['mV']
        }; // 0x0C

    sensors[13] = { code: 13, desc: 'Digital', shortDesc: 'Dig' };        // 0x0D

    sensors[21] = { code: 21, desc: 'Analog 7', shortDesc: 'Ana7',
                    scales: [0, 2],
                    legends: ['Voltage'],
                    units: ['V']
        }; // 0x15

    sensors[22] = { code: 22, desc: 'Analog 8', shortDesc: 'Ana8',
                    scales: [0, 2],
                    legends: ['Voltage'],
                    units: ['V']
        }; // 0x16

    sensors[23] = { code: 23, desc: 'Analog 9', shortDesc: 'Ana9',
                    scales: [0, 2],
                    legends: ['Voltage'],
                    units: ['V']
        }; // 0x17

    sensors[26] = { code: 26, desc: 'Thermometer', shortDesc: 'Therm',
                    pathColors: [colorBlue],
                    scales: [0, 40],
                    legends: ['Temp.'],
                    units: ['°C'],
                    fixedScaleY: true
        }; // 0x1A

    var fixedConfigs = {};
    fixedConfigs[1] = { desc: 'Gyroscope'};
    fixedConfigs[2] = { desc: 'Accelerometer'};
    fixedConfigs[3] = { desc: 'Orientation'};
    fixedConfigs[4] = { desc: 'Mini-motion'};
    fixedConfigs[5] = { desc: 'Pendulum'};
    fixedConfigs[6] = { desc: 'Ambient'};
    fixedConfigs[7] = { desc: 'ECG (requires plugin)' };
    fixedConfigs[8] = { desc: 'Header'};
    fixedConfigs[9] = { desc: 'Microphone'};
    fixedConfigs[10] = { desc: 'Magnetic' };                   // 0x0A
    /* ----------- High speed --------- */
    fixedConfigs[32] = { desc: 'Gyroscope (HS)' };             // 0x20
    fixedConfigs[33] = { desc: 'Accelerometer (HS)' };         // 0x21
    fixedConfigs[34] = { desc: 'Orientation (HS)' };           // 0x22
    fixedConfigs[35] = { desc: 'Motion' };                     // 0x23
    fixedConfigs[36] = { desc: 'Sports' };                     // 0x24
    fixedConfigs[37] = { desc: 'Pendulum (HS)' };              // 0x25
    fixedConfigs[38] = { desc: 'Kitchen Sink' };               // 0x26
    fixedConfigs[39] = { desc: 'Microphone (HS)' };            // 0x27
    fixedConfigs[40] = { desc: 'Ambient Light (HS)' };         // 0x28
    fixedConfigs[41] = { desc: 'Ambient Light & Accel (HS)' }; // 0x29
    fixedConfigs[42] = { desc: 'Force Gauge & Accel (HS)' };   // 0x2A
    fixedConfigs[43] = { desc: 'Ambient Light & Micro (HS)' }; // 0x2B
    fixedConfigs[44] = { desc: 'Electrocardiograph (HS)' };    // 0x2C
    fixedConfigs[45] = { desc: 'High Gain (HS)' };             // 0x2D
    fixedConfigs[46] = { desc: 'Force Gauge (HS)' };           // 0x2E

    var sensorConfigs = {};
    sensorConfigs[1]  = { configKey: 1, sensorKey: 3, sampleRate: 380 };
    sensorConfigs[2]  = { configKey: 2, sensorKey: 1, sampleRate: 400 };
    sensorConfigs[3]  = { configKey: 3, sensorKey: 1, sampleRate: 100 };
    sensorConfigs[4]  = { configKey: 3, sensorKey: 2, sampleRate: 80  };
    sensorConfigs[5]  = { configKey: 3, sensorKey: 3, sampleRate: 95  };
    sensorConfigs[6]  = { configKey: 3, sensorKey: 12, sampleRate: 100 };
    sensorConfigs[7]  = { configKey: 4, sensorKey: 1, sampleRate: 200 };
    sensorConfigs[8]  = { configKey: 4, sensorKey: 9, sampleRate: 100 };
    sensorConfigs[9]  = { configKey: 4, sensorKey: 8, sampleRate: 200 };
    sensorConfigs[10] = { configKey: 5, sensorKey: 1, sampleRate: 100 };
    sensorConfigs[11] = { configKey: 5, sensorKey: 3, sampleRate: 95  };
    sensorConfigs[12] = { configKey: 5, sensorKey: 8, sampleRate: 100 };
    sensorConfigs[13] = { configKey: 6, sensorKey: 4, sampleRate: 100 };
    sensorConfigs[14] = { configKey: 6, sensorKey: 11, sampleRate: 50  };
    sensorConfigs[15] = { configKey: 6, sensorKey: 7, sampleRate: 400 };
    sensorConfigs[16] = { configKey: 6, sensorKey: 26, sampleRate: 50  };
    sensorConfigs[17] = { configKey: 7, sensorKey: 10, sampleRate: 400 };
    sensorConfigs[18] = { configKey: 8, sensorKey: 21, sampleRate: 100 };
    sensorConfigs[19] = { configKey: 8, sensorKey: 22, sampleRate: 100 };
    sensorConfigs[20] = { configKey: 8, sensorKey: 23, sampleRate: 100 };
    sensorConfigs[21] = { configKey: 8, sensorKey: 12, sampleRate: 200 };
    sensorConfigs[22] = { configKey: 8, sensorKey: 13, sampleRate: 100 };
    sensorConfigs[23] = { configKey: 9, sensorKey: 6, sampleRate: 2400 };
    sensorConfigs[24] = { configKey: 10, sensorKey: 2, sampleRate: 80  };
    sensorConfigs[25] = { configKey: 10, sensorKey: 12, sampleRate: 400 };
    /* ----------- High speed --------- */
    sensorConfigs[26] = { configKey: 32, sensorKey: 3, sampleRate: 760 };
    sensorConfigs[27] = { configKey: 33, sensorKey: 1, sampleRate: 800 };
    sensorConfigs[28] = { configKey: 34, sensorKey: 1, sampleRate: 400 };
    sensorConfigs[29] = { configKey: 34, sensorKey: 2, sampleRate: 80  };
    sensorConfigs[30] = { configKey: 34, sensorKey: 3, sampleRate: 190 };
    sensorConfigs[31] = { configKey: 35, sensorKey: 1, sampleRate: 200 };
    sensorConfigs[32] = { configKey: 35, sensorKey: 3, sampleRate: 190 };
    sensorConfigs[33] = { configKey: 35, sensorKey: 9, sampleRate: 100 };
    sensorConfigs[34] = { configKey: 35, sensorKey: 8, sampleRate: 200 };
    sensorConfigs[35] = { configKey: 36, sensorKey: 10, sampleRate: 200 };
    sensorConfigs[36] = { configKey: 36, sensorKey: 1, sampleRate: 200 };
    sensorConfigs[37] = { configKey: 36, sensorKey: 2, sampleRate: 80  };
    sensorConfigs[38] = { configKey: 36, sensorKey: 3, sampleRate: 190 };
    sensorConfigs[39] = { configKey: 37, sensorKey: 1, sampleRate: 200 };
    sensorConfigs[40] = { configKey: 37, sensorKey: 3, sampleRate: 190 };
    sensorConfigs[41] = { configKey: 37, sensorKey: 8, sampleRate: 200 };
    sensorConfigs[42] = { configKey: 38, sensorKey: 2, sampleRate: 80  };
    sensorConfigs[43] = { configKey: 38, sensorKey: 1, sampleRate: 100 };
    sensorConfigs[44] = { configKey: 38, sensorKey: 9, sampleRate: 100 };
    sensorConfigs[45] = { configKey: 38, sensorKey: 8, sampleRate: 100 };
    sensorConfigs[46] = { configKey: 38, sensorKey: 3, sampleRate: 95  };
    sensorConfigs[47] = { configKey: 38, sensorKey: 7, sampleRate: 100 };
    sensorConfigs[48] = { configKey: 38, sensorKey: 11, sampleRate: 100 };
    sensorConfigs[49] = { configKey: 38, sensorKey: 12, sampleRate: 100 };
    sensorConfigs[50] = { configKey: 38, sensorKey: 21, sampleRate: 100 };
    sensorConfigs[51] = { configKey: 38, sensorKey: 13, sampleRate: 100 };
    sensorConfigs[52] = { configKey: 38, sensorKey: 4, sampleRate: 100 };
    sensorConfigs[53] = { configKey: 39, sensorKey: 6, sampleRate: 4800 };
    sensorConfigs[54] = { configKey: 40, sensorKey: 7, sampleRate: 4800 };
    sensorConfigs[55] = { configKey: 41, sensorKey: 7, sampleRate: 800 };
    sensorConfigs[56] = { configKey: 41, sensorKey: 1, sampleRate: 800 };
    sensorConfigs[57] = { configKey: 42, sensorKey: 8, sampleRate: 800 };
    sensorConfigs[58] = { configKey: 42, sensorKey: 1, sampleRate: 800 };
    sensorConfigs[59] = { configKey: 43, sensorKey: 7, sampleRate: 2400 };
    sensorConfigs[60] = { configKey: 43, sensorKey: 6, sampleRate: 2400 };
    sensorConfigs[61] = { configKey: 44, sensorKey: 10, sampleRate: 800 };
    sensorConfigs[62] = { configKey: 45, sensorKey: 12, sampleRate: 4800 };
    sensorConfigs[63] = { configKey: 46, sensorKey: 8, sampleRate: 4800 };


    this.getDefaultColor = function(arg) {
        var defaultColor = [colorRed, colorBlue, colorGreen];
        if (!arguments.length) {
            return defaultColor;
        }
        return defaultColor[arg % 3];
    };

    this.getDongleMode = function (code) { return dongleModes[code]; };
    this.getSensor = function (code) { return sensors[code]; };

    this.getAreaUnit = function (unit) {
        if (!unit) {
            return '';
        }
        switch(unit) {
            case 'm/s': return 'm';
            case 'm/s²': return 'm/s';
            case 'rad/s': return 'rad';
        }
        return unit + 's';
    };

    /**
     * @param sensorKey, configKey
     * @returns {string}
     */
    this.getSampleRate = function (sensorKey, configKey) {
        var sampleRate = '';
        angular.forEach(sensorConfigs, function (value, key) {
            if (value.sensorKey == sensorKey && value.configKey == parseInt(configKey, 10)) {
                sampleRate = value.sampleRate;
            }
        });
        return sampleRate;
    };

    /**
     *  Sensor array of {code, desc}
     *
     * @returns {Array}
     */
    this.getSensors = function () {
        var sens = [], key;
        for (key in sensors) {
            sens.push({ code: key, desc: sensors[key].desc });
        }
        return sens;
    };

    /**
     *  Config array of {code, desc}
     *
     * @returns {Array}
     */
    this.getConfigs = function () {
        var configs = [], key;
        for (key in fixedConfigs) {
            configs.push({ code: key, desc: fixedConfigs[key].desc });
        }
        return configs;
    };

    /**
     *  Sensor Config array of {code, configKey, sensorKey, sampleRate}
     *
     * @returns {Array}
     */
    this.getSensorConfigs = function () {
        var sensConfigs = [], key;
        for (key in sensorConfigs) {
            sensConfigs.push({
                code: key,
                configKey: sensorConfigs[key].configKey,
                sensorKey: sensorConfigs[key].sensorKey,
                sampleRate: sensorConfigs[key].sampleRate
            });
        }
        return sensConfigs;
    };

    // Return an array containing objects as {legend: 'a_x (m/s2)', color: '#BB0000'} for each components (eg: x, y, z).
    // This array is be used to show/hide chart lines
    this.getLegendSettings = function (sensorType) {
        var legendSettings = [];
        var sensor = sensors[sensorType];
        if (sensor.legends) {
            sensor.legends.forEach(function (item, idx) {
                legendSettings.push({
                    legend: item + ((sensor.units || [])[idx] ? ' (' + sensor.units[idx] + ')' : ''),
                    color: sensor.pathColors ? sensor.pathColors[idx] : self.getDefaultColor(idx)
                });
            });
        }
        return legendSettings;
    };
});

angular.module('IOLabApp').factory('helperService', function () {
    'use strict';
    var exports = {},
        d0 = function (d) { return d[0]; };

    // Return the index in data array for the input time
    exports.getDataIndex = function (data, time) {

        if (!data || !data.length) {
            return -1;
        }

        var index = d3.bisector(d0).left(data[0], time);

        // Limit cases
        if (index === 0) {
            return index;
        }
        if (index === data[0].length) {
            return index - 1;
        }

        // Find the nearest point
        if (time - data[0][index - 1][0] <= data[0][index][0] - time) {
            return index - 1;
        }
        return index;
    };

    // Format the current date & time as a string: eq "20140219223933"
    exports.getTime = function () {

        var now = new Date(),
            format = function (element) {
                return (element < 10) ? ("0" + element) : element.toString();
            };

        return (now.getFullYear().toString()
            + format(now.getMonth() + 1)
            + format(now.getDate())
            + format(now.getHours())
            + format(now.getMinutes())
            + format(now.getSeconds()));
    };

    // Brut force copy is much more efficient than
    // usual deep object copy
    exports.copyData = function (dataOrig, data) {
        var i, n, len;

        // Total number of data points
        len = dataOrig[0].length;

        for (i = 0; i < data.length; i++) {
            for (n = 0; n < len; n++) {
                data[i][n][1] = dataOrig[i][n][1];
            }
        }
    };

    exports.smoothData = function (dataOrig, data, delta) {
        var i, j, n, len, sum;
        // Total number of data points
        len = dataOrig[0].length;

        for (n = 0; n < dataOrig.length; n++) {
            for (i = 0; i < delta; i++) {
                for (sum = 0, j = 0; j < i + delta + 1; j++) {
                    sum += dataOrig[n][j][1];
                }
                data[n][i][1] = sum / (i + delta + 1);
            }
            for (i = delta; i < len - delta; i++) {
                for (sum = 0, j = i - delta; j < i + delta + 1; j++) {
                    sum += dataOrig[n][j][1];
                }
                data[n][i][1] = sum / (2 * delta + 1);
            }
            for (i = len - delta; i < len; i++) {
                for (sum = 0, j = i - delta; j < len; j++) {
                    sum += dataOrig[n][j][1];
                }
                data[n][i][1] = sum / (len - i + delta);
            }
        }
    };

    exports.computeFFT = function (data, index, bufferSize) {

        var dataLen = data[0].length,
            fftSpectrums = [],
            frequency;

        if (dataLen < bufferSize || bufferSize === null) {
            return null;
        }

        // Force start to 0 or end to bufferSize if the selection
        // is out of the chart
        index = Math.min(index, dataLen - bufferSize / 2 - 1);
        index = Math.max(bufferSize / 2, index);

        // Chart data frequency
        frequency = dataLen / (data[0][dataLen - 1][0] - data[0][0][0]);

        data.forEach(function (pathData) {

            var signal = pathData.slice(index - bufferSize / 2, index + bufferSize / 2)
                .map(function (item) {
                    return item[1];
                });

            var fft = new FFT(bufferSize, frequency);
            fft.forward(signal);

            var fftSpectrum = [];
            angular.forEach(fft.spectrum, function (value, key) {
                fftSpectrum[key] = [frequency * key / bufferSize, value];
            });

            fftSpectrums.push(fftSpectrum);
        });

        return {
            index: index,
            frequency: frequency,
            samplingRate: bufferSize,
            spectrums: fftSpectrums
        };
    };

    exports.bbox = function (data, scale, sensorParams) {

        var res = {},
            len = data[0].length,
            deltaY,
            y0,
            y1;

        if (len == 0) {
            return scale;
        }

        // x scale ...
        res.x0 = 0;
        res.x1 = data[0][len - 1][0];

        // y scale if not fixed !
        if (!sensorParams.fixedScaleY) {

            var factorScale = sensorParams.scaleFactors || [1];

            y0 = Math.min.apply(Math, data.map(function (d, idx) {
                // Math.min.apply fails due to the length of the data vector...
                var min = Infinity, i = len,
                    factor = factorScale[idx % factorScale.length];
                while (i--) { if (d[i][1] * factor < min) { min = d[i][1] * factor; } }
                return min;
            }));


            y1 = Math.max.apply(Math, data.map(function (d, idx) {
                // Math.min.apply fails due to the length of the data vector...
                var max = -Infinity, i = len,
                    factor = factorScale[idx % factorScale.length];
                while (i--) { if (d[i][1] * factor > max) { max = d[i][1] * factor; } }
                return max;
            }));

            deltaY = y1 - y0 || 10;
            res.y0 = y0 - deltaY / 4;
            res.y1 = y1 + deltaY / 4;

        } else {
            res.y0 = scale.y0;
            res.y1 = scale.y1;
        }

        return res;
    };

    return exports;
});

angular.module('IOLabApp').factory('chartModeService', function () {
    'use strict';

    var exports =  function () {

        var chartMode = {
            scales: []
        };

        chartMode.set = function (key) {
            chartMode.stats = false;
            chartMode.move = false;
            chartMode.zoom = false;

            // Set value key of default value
            chartMode[key || 'zoom'] = true;
        };

        chartMode.get = function () {
            Object.keys(chartMode).forEach(function (mode) {
                if (chartMode[mode] === true) {
                    return mode;
                }
            });
        };

        chartMode.set();
        return chartMode;
    };

    return exports;

});

angular.module('IOLabApp').factory('selectionService', function () {
    'use strict';

    var exports =  function () {

        var selection = {
            p0: { x: -1, y: -1, index: -1, locked: false },
            p1: { x: -1, y: -1, index: -1, locked: false }
        };

        selection.reset = function () {
            ['p0', 'p1'].forEach(function (key) {
                selection[key].x = -1;
                selection[key].y = -1;
                selection[key].index = -1;
                selection[key].locked = false;
            });
        };

        selection.notNull = function () {
            if (selection.p1.x === -1 || selection.p0.x === -1 ||
                    selection.p1.y === -1 || selection.p0.y === -1) {
                return false;
            }
            return selection.p0.x !== selection.p1.x ||
                selection.p0.y !== selection.p1.y;
        }

        return selection;
    };
    return exports;
});

angular.module('IOLabApp').factory('chartComService', function () {
   'use strict';

    var closure = function () {

        var exports = {};

        return exports;
    };

    return closure;
});
