angular.module('IOLabApp', ['ui.bootstrap', 'ngSanitize', 'infinite-scroll']);

angular.module('IOLabApp').constant('mgtCMD', {
    GetList: 0,
    Get: 1,
    Save: 2,
    Delete: 3,
    Reload: 4,
    Export: 7
});

angular.module('IOLabApp').service('IOLabMode', function() {
    'use strict';
    
    var acquisitionMode = 'acquisition',
        playerMode = 'lessonPlayer',
        currentMode = acquisitionMode;
    
    this.setModeAcquisition = function() {
        currentMode = acquisitionMode;
    };
    this.setModeLessonPlayer = function() {
        currentMode = playerMode;
    };
    this.isAcquisitionMode = function() {
        return currentMode == acquisitionMode
    };
    this.isLessonPlayerMode = function() {
        return currentMode == playerMode;
    };
});

angular.module('IOLabApp').run(function ($rootScope, $window, $document, $timeout, IOLab, IOLabMode, IOLabVersion, Logger, configService) {
    'use strict';

    // Note that $document doesn't work => must use 'document'
    document.title = IOLabVersion.appTitle;

    $window.Object.defineProperty(Element.prototype, 'documentOffsetTop', {
        get: function () { return this.offsetTop + ( this.offsetParent ? this.offsetParent.documentOffsetTop : 0 ); }
    });

    angular.element($window).on('resize', function () {
        $rootScope.$broadcast('recompute-chart-height');
    });

    // Expose the IOLabEventHandler object to the global scope!
    // This function is called from the CEF Application depending on signals
    // sent by the API
    $window.IOLabEventHandler = {
        sendEvent: function (_event) {
            var event = _event || {};

            Logger.info('>>> IOLabEventHandler', event.name, event.args || '');

            switch (event.name) {
            case 'hardwareStateChanged':
                IOLab.getHardwareState();
                break;

            case 'acquisitionStackChanged':
                IOLab.getAcquisition();
                break;

            case 'dataStarted':
            case 'dataStopped':
                if ($rootScope.rezeroing) break;
                $rootScope.$broadcast(event.name, event.args);
                $rootScope.$$phase || $rootScope.$apply();
                break;

            case 'findRemoteResult':
            case 'sensorRecalibrated':
            case 'sensorRezeroing':
            case 'fixedConfigChanged':
            case 'repository':
            case 'openAboutModal':
                $rootScope.$broadcast(event.name, event.args);
                $rootScope.$$phase || $rootScope.$apply();
                break;

            default:
                Logger.info('>>> onEvent', event.name, event.args || '');
            }
        }
    };

    // FIXME - Take into account the loaded report ?
    IOLab.clearReportID();
    IOLab.getAcquisition()
        .then(function() {
            if (!IOLab.isNewAcquisition()) {
                IOLab.pushAcquisitionSetRun();
            }
        });
    IOLab.getHardwareState();

    $rootScope.IOLab = IOLab;
    $rootScope.IOLabMode = IOLabMode;

    $rootScope.displayMode = { value: 1 };

    $rootScope.$on('hardwareStateChanged', function () {
        $rootScope.iolabState = IOLab.hardwareState();
        $rootScope.dongleMode = configService.getDongleMode($rootScope.iolabState.dongleMode).desc;

        $rootScope.r1Off = IOLab.hardwareState(1).rfStatus === 0;
        $rootScope.r2Off = IOLab.hardwareState(2).rfStatus === 0;
        $rootScope.r1Configured = IOLab.hardwareState(1).configured;
        $rootScope.r2Configured = IOLab.hardwareState(2).configured;
        $rootScope.r1Disabled = $rootScope.r1Off || $rootScope.r1Configured;
        $rootScope.r2Disabled = $rootScope.r2Off || $rootScope.r2Configured;
    });
});

// The "app" listener is accessible within the CEF application.
// It is used to communicate with the CEF client.
angular.module('IOLabApp').factory('IOLabAPI', function (BSON, Logger) {
    'use strict';

    var exports = {},
        node = _.get(window, 'process.versions.node') ? {} : null;

    /*
     * Helper method to build the buffer array...
     */
    function dezerialize(bson, dataBuffer) {

        var len, i, n,
            bufferInit8,
            bufferFloat32,
            t1 = new Date();

        if (!bson) {
            return;
        }

        if (typeof bson === 'string') {
            len = bson.length;
            bufferInit8 = new Uint8Array(len);
            for (i = 0; i < len; i++) {
                bufferInit8[i] = bson.charCodeAt(i);
            }
        } else {
            bufferInit8 = bson;
        }

        var json = BSON.deserialize(bufferInit8);

        // Deserialize the data buffer
        if (dataBuffer && json.datalength && !!json.sampleSize) {

            if (typeof dataBuffer === 'string') {
                len = dataBuffer.length;
                bufferInit8 = new Uint8Array(len);
                for (i = 0; i < len; i++) {
                    bufferInit8[i] = dataBuffer.charCodeAt(i);
                }
            } else {
                bufferInit8 = dataBuffer;
            }

            // Create a view on the float array
            bufferFloat32 = new Float32Array(bufferInit8.buffer);

            // Build the data array with the data
            // For parametric plot, only 1 vector with elements [t, s1a, s1b, s1c, s2, ...]
            // For charts, one vector with elements [t, cal_x, (raw_x)] for each sensor path
            json.data = [];

            // Param plot
            if (json.sampleSize instanceof Array) {

                for (i = 0; i < json.datalength; i++) {
                    var data = [];
                    for (n = 0; n < json.sampleSize[0] + json.sampleSize[1] + 1; n++) {
                        data.push(bufferFloat32[n * json.datalength + i]);
                    }
                    json.data.push(data);
                }

            } else {
            // Chart

                // Initialisation fo the vector of vector
                for (n = 0; n < json.sampleSize; n++) {
                    json.data[n] = [];
                }

                for (i = 0; i < json.datalength; i++) {
                    for (n = 0; n < json.sampleSize; n++) {
                        if (!json.rawdata) {
                            json.data[n].push([
                                bufferFloat32[i],
                                bufferFloat32[(n + 1) * json.datalength + i]
                            ]);
                        } else {
                            json.data[n].push([
                                bufferFloat32[i],
                                bufferFloat32[(n + 1) * json.datalength + i],
                                bufferFloat32[(n + 1 + json.sampleSize) * json.datalength + i]
                            ]);
                        }
                    }
                }
            }
        }

        var t2 = new Date(),
            delta_t = t2 - t1,
            size_kb = parseInt(((bufferFloat32 || []).length / 256).toFixed(0), 10);
        if (delta_t > 10 && bufferFloat32)
            Logger.info('*** Data deserialization:', delta_t, 'ms -', size_kb, 'kB');

        return json;
    }

    /*
     *  Node implementation
     */

    if (node) {
        Logger.info('+++ Node - Electron.io');

        node.ipc = require("electron").ipcRenderer;
        node.ipcID = 1;
        node.callbackStack = [];

        node.register = function (message) {
            return function () {
                var args = [].slice.call(arguments);
                Logger.info('<<< ipc.send', message, args);
                node.ipc.send('iolab', message, null, args);
            };
        };

        node.registerCallback = function (message) {
            return function () {
                var ipcId = (node.ipcID)++,
                    args = [].slice.call(arguments),
                    callback = args.shift();

                if (message != 'getSensorData')
                    Logger.info('<<< ipc.send', message, ipcId, args);

                node.ipc.send('iolab', message, ipcId, args);
                node.callbackStack.push({
                    ipcId: ipcId,
                    callback: callback
                });
            };
        };

        node.ipc.on('iolab-reply', function (event, args) {
            var cbEntry = _.find(node.callbackStack, { ipcId: args.ipcId });
            if (cbEntry) {
                cbEntry.callback(dezerialize(args.bson, args.buffer));
                _.pull(node.callbackStack, cbEntry);
            }
        });

        var iolabMethods = [
            { msg: 'getHardwareState', callback: true },
            { msg: 'getAcquisition', callback: true },

            { msg: 'setFixedConfig', callback: true },
            { msg: 'resetAcquisition', callback: true },
            { msg: 'setReportID' },

            { msg: 'manageAcquisitions', callback: true },
            { msg: 'manageCalibrations', callback: true },
            { msg: 'manageLessons', callback: true },
            { msg: 'manageReports', callback: true },

            { msg: 'getSensorData', callback: true },
            { msg: 'getSensorData2', callback: true },

            { msg: 'resetSensorOffset', callback: true },
            { msg: 'recalibrateSensor', callback: true },

            { msg: 'exportData', callback: true },

            { msg: 'startDataAcquisition' },
            { msg: 'stopDataAcquisition' },
            { msg: 'remotePowerOff' },
            { msg: 'pairRemote' },
            { msg: 'setOutputConfig' },
            { msg: 'sendCommand'},
            { msg: 'broadcast'}
        ];

        _.forEach(iolabMethods, function(m) {
            exports[m.msg] = m.callback
                ? node.registerCallback(m.msg)
                : node.register(m.msg);
        });


        //
        // Report management (Electron side)
        //
        exports.exportReport = function (reportID) {
            node.ipc.send('export','Report', reportID);
        };

        exports.importReport = function (callback) {
            node.ipc.once('import-reply', function(event, args) {
                callback(dezerialize(args.bson));
            });
            node.ipc.send('import','Report');
        };

        exports.confirmImportReport = function (callback, reportID) {
            node.ipc.once('confirm-import-reply', function(event, args) {
                callback(args);
            });
            node.ipc.send('confirm-import','Report', reportID);
        };

        // Copy an acquisition raw data from activity to report
        exports.copyAcquisitionToReport = function (callback, acquisitionPath, acqID, reportID) {
            node.ipc.once('copy-acquisition-to-report-reply', function(event, args) {
                callback(args);
            });
            node.ipc.send('copy-acquisition-to-report', acquisitionPath, acqID, reportID);
        };

        // Open the IOLab repository
        exports.openIOLabRepository = function () {
            node.ipc.send('openIOLabRepository');
        };

        exports.focusIOLabRepository = function () {
            node.ipc.send('focusIOLabRepository');
        };

    }

    return exports;
});

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

    /*
     * Private Members
     */

    var hardwareState = {
        remote1: { rfStatus: 0, configured: false, fixedConfig: 0, outputConfig: [] },
        remote2: { rfStatus: 0, configured: false, fixedConfig: 0, outputConfig: [] }
    };

    var acquisition = {
        remote1: { sensors: [] },
        remote2: { sensors: [] }
    };

    var acquisitionSet = {
        fixedConfig: {},
        duration: null,
        runCount: 0,
        runs: []
    };

    var exports = {};

    /*
     * Public methods
     */

    exports.waitEvent = function (eventName, condition) {
        var deferred = $q.defer();
        var cancel = $rootScope.$on(eventName, function(event, args) {
            if (condition && !condition(args)) return;
            cancel();
            deferred.resolve();
        });
        return deferred.promise;
    };

    //
    // Acquisition set
    //
    exports.initAcquisitionSet = function() {
        acquisitionSet.fixedConfig = {};
        //acquisitionSet.duration = null;
        acquisitionSet.runCount = 0;
        acquisitionSet.runs = [];

        // Reset common chart scale values
        sharedChartService.init();
    };

    exports.newAcquisitionSetRun = function() {
        // Setup new run only if the current acquisition contains data
        if (exports.isNewAcquisition()) {
            return $q.when();
        }

        var deferred = $q.defer();
        var newRun = true;

        // Resolve: { success: true/false }
        IOLabAPI.resetAcquisition(deferred.resolve, newRun);
        return deferred.promise;
    };

    exports.pushAcquisitionSetRun = function (_acqID, opts = {}) {
        var acqID = _acqID || acquisition.ID,
            run = _.find(acquisitionSet.runs, { acqID: acqID }),
            newRun = { acqID: acqID };

        if (run) { return; }

        if (opts.template) {
            // Template runs are readonly!
            newRun.name = 'Template';
            newRun.readOnly = true;
        } else {
            // Increase run count used to create run name
            newRun.name = 'Run' + (++acquisitionSet.runCount);
        }

        acquisitionSet.runs.push(newRun);

        $rootScope.$broadcast('acquisitionSet.newRun', { acqID: acqID });
    };

    exports.removeAcquisitionSetRun = function(runToRemove) {
        _.remove(acquisitionSet.runs, function(run) {
            return run.acqID === runToRemove.acqID; 
        });

        //  Inform relevant instance (chart.js for instance) that a run has been removed
        $rootScope.$broadcast('acquisitionSet.removeRun', { acqID: runToRemove.acqID });

        if(runToRemove.acqID === acquisition.ID){
            exports.newAcquisitionSetRun();
        }
    };

    exports.acquisitionSet = function() {
        return acquisitionSet;
    };

    exports.setAcquisitionSet = function(acqSet) {
        angular.copy(acqSet, acquisitionSet);
    };

    //
    // Hardware state
    //
    exports.hardwareState = function (remoteIdx) {
        if (remoteIdx) {
            return hardwareState['remote' + remoteIdx] || {};
        }
        return hardwareState;
    };

    exports.remotesState = function () {
        return {
            r1Off: hardwareState.remote1.rfStatus === 0,
            r2Off: hardwareState.remote2.rfStatus === 0
        };
    };

    //
    // Acquisition
    //
    exports.acquisition = function (remoteIdx) {
        if (remoteIdx) {
            return acquisition['remote' + remoteIdx] || {};
        }
        return acquisition;
    };

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

    // Return true if the current acquisition duration has reach the acquisitionSet duration (if any)
    exports.currentRunIsComplete = function () {
        return acquisitionSet.duration && acquisition.duration >= acquisitionSet.duration;
    };


    //
    // Output config
    //

    // return the value of the output config for the specified remote, sensor and key
    exports.outputConfigValue = function (remote, sensor, key) {
        var sensorOutputConfig = _.find(exports.hardwareState(remote).outputConfig, { sensor: sensor });
        if (!sensorOutputConfig) return -1;
        var keyValue = _.find(sensorOutputConfig.keyValues, { key: key });
        if (!keyValue || keyValue.value === undefined) return -1;
        return keyValue.value;
    };


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

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

    exports.getHardwareState = function () {
        var deferred = $q.defer();
        IOLabAPI.getHardwareState(function (json) {
            var oldHardwareState = angular.copy(hardwareState);
            hardwareState = json;
            console.log('>>> hardwareState', hardwareState);
            $rootScope.$broadcast('hardwareStateChanged', hardwareState, oldHardwareState);
            $rootScope.$$phase || $rootScope.$apply();
            deferred.resolve(json);
        });
        return deferred.promise;
    };

    exports.getAcquisition = function () {
        var deferred = $q.defer();
        IOLabAPI.getAcquisition(function (json) {
            var oldAcquisition = angular.copy(acquisition);
            acquisition = json;
            console.log('>>> acquisition', acquisition);
            $rootScope.$broadcast('acquisitionStackChanged', acquisition, oldAcquisition);
            $rootScope.$$phase || $rootScope.$apply();
            deferred.resolve(json);
        });
        return deferred.promise;
    };

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

    exports.setFixedConfig = function (remote, config) {
        var deferred = $q.defer();
        // Resolve: { success: true/false }
        IOLabAPI.setFixedConfig(function (result) {
            if (!result.success) {
                console.log('ERROR - setFixedConfig');
                deferred.reject();
                return;
            }

            // There is no command sent to the dongle in case of config 0
            // No need to wait the 'fixedConfigChanged' triggered by GET_FIXED_CONFIG
            if (config === 0) {
                deferred.resolve();
                return;
            }

            exports.waitEvent('fixedConfigChanged', (args) => { return args.remote === remote; })
                .then(function() {
                    // Keep the fixed config setting in the acquisitionSet!
                    acquisitionSet.fixedConfig[remote] = config;
                    deferred.resolve();
                });

        }, remote, config);
        return deferred.promise;
    };

    exports.setOutputConfig = function (remote, sensor, key, value) {
        IOLabAPI.setOutputConfig(remote, sensor, key, value);
    };

    exports.startData = function (duration) {
        exports.pushAcquisitionSetRun();

        IOLabAPI.startDataAcquisition(duration || undefined);
    };

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

    exports.resetSensorOffset = function (remote, sensor, start, end) {
        var deferred = $q.defer();
        IOLabAPI.resetSensorOffset(deferred.resolve, remote, sensor, start, end);
        return deferred.promise;
    };

    exports.resetSensorOffset2 = function (remote, sensor) {
        var deferred = $q.defer();
        $rootScope.rezeroing = true;

        IOLabAPI.resetSensorOffset(function () {
            var condition = function(args) {
                return args.remote === remote && args.sensorType === sensor;
            };
            return $q.when()
                .then(function() {
                    return exports.waitEvent('sensorRezeroing', condition);
                })
                .then(function(){
                    return exports.waitEvent('acquisitionStackChanged');
                })
                .then(function() {
                    $rootScope.rezeroing = false;
                    deferred.resolve();
                });
        }, remote, sensor);
        return deferred.promise;
    };

    exports.recalibrateSensor = function (remote, sensor) {
        var deferred = $q.defer();
        if (exports.isNewAcquisition()) {
            deferred.resolve();
        } else {
            IOLabAPI.recalibrateSensor(deferred.resolve, remote, sensor);
        }
        return deferred.promise;
    };

    exports.exportData = function (remote, sensor, fileName) {
        var deferred = $q.defer();
        // resolve : { success: true,  path: String}
        // reject  : { success: false, error: String }
        IOLabAPI.exportData(function(res) {
            deferred[res.success ? 'resolve' : 'reject'](res);
        }, remote, sensor, fileName || '');
        return deferred.promise;
    };

    exports.getSensorData = function (callback, args) {
        return IOLabAPI.getSensorData(
            callback,
            args.remote,
            args.sensor,
            args.startTime || 0,
            args.endTime || -1,
            args.rawData || false,
            args.coordinate === undefined ? -1 : args.coordinate,
            args.frequency || -1,
            args.acqID || ''
        );
    };

    exports.getSensorData2 = function (callback, args) {
        return IOLabAPI.getSensorData2(
            callback,
            args.remote,
            args.sensor1,
            args.sensor2,
            args.startTime || 0,
            args.endTime || -1,
            args.coordinate1 === undefined ? -1 : args.coordinate1,
            args.coordinate2 === undefined ? -1 : args.coordinate2
        );
    };

    /*
     * Acquisition storage management (local raw/bson files)
     */

    exports.manageAcquisitions = function (cmd, acqID, acquisition) {
        var deferred = $q.defer();
        if (acquisition) {
            var bson = BSON.serialize(acquisition);
            IOLabAPI.manageAcquisitions(deferred.resolve, cmd, acqID, bson);
        } else {
            IOLabAPI.manageAcquisitions(deferred.resolve, cmd, acqID);
        }
        return deferred.promise;
    };

    exports.manageCalibrations = function (cmd, calibration) {
        var deferred = $q.defer();
        if (calibration) {
            var bson = BSON.serialize(calibration);
            IOLabAPI.manageCalibrations(deferred.resolve, cmd, bson);
        }
        else {
            IOLabAPI.manageCalibrations(deferred.resolve, cmd);
        }
        return deferred.promise;
    };

    exports.manageLessons = function (cmd) {
        var deferred = $q.defer();
        IOLabAPI.manageLessons(deferred.resolve, cmd);
        return deferred.promise;
    };

    exports.manageReports = function (cmd, repID, report) {
        var deferred = $q.defer();
        if (report) {
            var bson = BSON.serialize(report);
            IOLabAPI.manageReports(deferred.resolve, cmd, repID, bson);
        } else {
            IOLabAPI.manageReports(deferred.resolve, cmd, repID);
        }
        return deferred.promise;
    };

    /*
     * Acquisition management
     */

    exports.resetAcquisition = function () {
        var deferred = $q.defer();

        // Init acquisition set        
        exports.initAcquisitionSet();

        // Resolve: { success: true/false }
        IOLabAPI.resetAcquisition(deferred.resolve);
        return deferred.promise;
    };

    exports.setReportID = function (reportID) {
        if (!reportID)
            IOLabAPI.setReportID(true);
        else {
            IOLabAPI.setReportID(reportID);
        }
    };

    exports.clearReportID = function () {
        IOLabAPI.setReportID(false);
    };

    exports.exportReport = function (reportID) {
        IOLabAPI.exportReport(reportID);
    };

    exports.importReport = function () {
        var deferred = $q.defer();
        IOLabAPI.importReport(deferred.resolve);
        return deferred.promise;
    };

    exports.confirmImportReport = function (reportID) {
        var deferred = $q.defer();
        IOLabAPI.confirmImportReport(deferred.resolve, reportID);
        return deferred.promise;
    };

    exports.copyAcquisitionToReport = function (acquisitionPath, acqID, reportID) {
        var deferred = $q.defer();
        IOLabAPI.copyAcquisitionToReport(deferred.resolve, acquisitionPath, acqID, reportID);
        return deferred.promise;
    };

    /*
     * IOLabRepository management
     */

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

    exports.exportToRepository = function(frequency, sensors, snapshots) {

        var args = JSON.stringify({ 
            frequency: frequency || 100,
            scale: { x0: 0, x1: -1 },
            sensors: sensors,
            snapshots: snapshots
        });

        IOLabAPI.focusIOLabRepository();
        IOLabAPI.broadcast('exportToRepository', args);
    };

    return exports;
});

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

    const remote = require('electron').remote;
    const logger = remote.getGlobal('logger');


    function info() {
        var argList = _.map([].slice.call(arguments), function(item) {
            return typeof item == 'object' ? JSON.stringify(item) : item;
        });

        // Log to file and node console
        logger.info(argList.join(' '));

        // Log to devTools console
        console.log.apply(console, arguments);
    }

    return {
        info: info
    }
});
angular.module('IOLabApp').controller('alertCtrl', function ($scope) {
    'use strict';
    $scope.alerts = [];

    $scope.$on('alert', function (event, args) {
        $scope.alerts.push(args);
    });

    $scope.addAlert = function (message, type) {
        $scope.alerts.push({message: message, type: type});
    };

    $scope.closeAlert = function (index) {
        $scope.alerts.splice(index, 1);
    };
});

angular.module('IOLabApp').controller('ModalPairingCtrl', function ($scope, $modal) {
    'use strict';
    $scope.open = function (remoteNum) {
        var modalInstance = $modal.open({
            templateUrl: 'views/modal-pairing.html',
            backdrop: 'static',
            controller: function ($scope, $modalInstance, $timeout, IOLab, configService) {

                $scope.closeWhenPaired = false;

                $scope.remoteID = 0;
                $scope.remoteNum = remoteNum;

                $scope.iolabState = IOLab.hardwareState();
                $scope.dongleMode = configService.getDongleMode($scope.iolabState.dongleMode).desc;

                $scope.getPairStatus = function (remote) {
                    return $scope.iolabState['remote' + remote].pairStatus;
                };

                $scope.pairRemote = function (remote) {
                    IOLab.pairRemote(parseInt(remote, 10), 1, $scope.remoteID);
                    $scope.closeWhenPaired = true;

                    // If not paired after 250ms, stop the pairing
                    $timeout(function () {
                        if ($scope.getPairStatus(remote) !== 1) {
                            $scope.stopPairing();
                            $scope.showFindRemoteMsg = true;
                            $scope.remoteID = 0;
                        } else {
                            $modalInstance.close();
                        }
                    }, 250);
                };

                $scope.unpairRemote = function (remote) {
                    $scope.showFindRemoteMsg = false;
                    IOLab.pairRemote(parseInt(remote, 10), 0);
                };

                $scope.findRemote = function () {
                    IOLab.sendCommand(19); // cmd 0x13
                    $timeout(function () {
                        $scope.showFindRemoteMsg = true;
                    }, 250);
                };

                $scope.stopPairing = function () {
                    IOLab.sendCommand(17); // cmd 0x11
                };

                $scope.close = function () {
                    $modalInstance.close();
                };

                $scope.$on('hardwareStateChanged', function () {
                    $scope.iolabState = IOLab.hardwareState();
                    $scope.dongleMode = configService.getDongleMode($scope.iolabState.dongleMode).desc;

                    if ($scope.closeWhenPaired && $scope.getPairStatus(remoteNum) === 1) {
                        $modalInstance.close();
                    }
                });

                $scope.$on('findRemoteResult', function (event, arg) {
                    $scope.remoteID = parseInt(arg, 10);
                });

                // Find remote
                IOLab.sendCommand(19); // cmd 0x13
            }
        });
    };
});


angular.module('IOLabApp').filter('customPropsFilter', function (){
    return function (items, prop) {

        if (!_.isArray(items) || !prop.Fields || !_.isArray(prop.Fields) || !prop.Query || prop.Query === '"')
            return items;

        //  First we split the Query string according to white spaces outside bounding " ... " and
        //  pushed the smallest word combination in wordBasis

        var out = [], wordBasis = [];

        _.forEach(prop.Query.split('"').join('"@#').split('"'), function (item, idx, array) {
            if (item.substring(0, 2) !== '@#') {
                _.forEach(item.trim().toLowerCase().split(' '), function (subItem) {
                    wordBasis.push(subItem);
                });
            } else if (array[idx + 1] && array[idx + 1].substring(0, 2) === '@#') {
                wordBasis.push(item.substring(2).trim().toLowerCase());
            } else {
                _.forEach(item.substring(2).trim().toLowerCase().split(' '), function (subItem) {
                    wordBasis.push(subItem);
                });
                wordBasis = _.filter(wordBasis, Boolean);
            }
        });

        //  Then we check possible matches

        _.map(items, function (item) {

            var itemMatches = false;

            _.every(wordBasis, function (queryString) {
                var match = _.find(prop.Fields, function (key) {
                    var items = _.get(item, key) || '';
                    // TODO : check if items/subStr are not objects ?
                    if (_.isArray(items))
                        return !!_.find(items, function (subStr) {
                            return subStr.toString().toLowerCase().indexOf(queryString) > -1;
                        });
                    else
                        return items.toString().toLowerCase().indexOf(queryString) > -1;
                });

                itemMatches = !!match;

                return itemMatches;
            });

            if (itemMatches) {
                out.push(item);
            }
        });

        return out;
    }
});
angular.module('IOLabApp').controller('gaugeCtrl', function ($scope, IOLab, configService) {
    'use strict';
    $scope.sensorDesc = configService.getSensor($scope.sensor.sensorType).desc;
});

angular.module('IOLabApp').directive('iolabGauge', function ($timeout, IOLab, BSON, gaugeService, configService) {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        controller: 'gaugeCtrl',
        template:
            '<div>' +
            '   <h5>{{sensorDesc}}</h5>' +
            '   <div class="gauge"></div>' +
            '</div>',
        link: function (scope, element, attrs) {

            var remote = scope.sensor.remote,
                sensor = scope.sensor.sensorType,
                sampleSize = scope.sensor.sampleSize;

            var gauges = [];
            var gaugeElement = d3.select(element[0]);

            var createGauge = function (name, label) {

                var config = {
                    size: 150,
                    label: label,
                    min: -20,
                    max: 20,
                    minorTicks: 5
                };

                gauges[name] = gaugeService(name, config);

                d3.select(element[0]).each(function () {
                    gauges[name].render(this);
                });
            };

            var gaugeName = function (nbr) {
                return 'gauge-' + remote + '-' + sensor + '-' + nbr;
            };

            // Create the html tag and gauge object
            for (var i = 0; i < sampleSize; i++) {
                gaugeElement.select('.gauge').append("span").attr('id', gaugeName(i));
                createGauge(
                    gaugeName(i),
                    configService.getSensor(sensor).shortDesc + '-' + ((sampleSize == 1) ? 'y' : ((i == 0) ? 'x' : ((i == 1) ? 'y' : 'z')))
                );
            }

            var timeoutId, timeInteval = 50;
            var newData = [], dataLength, startTime = 0;

            function addDataToChart(json) {
                newData = json;

                if (!newData.datalength) {
                    updateChart();
                    return;
                }

                dataLength = newData.data[0].length;
                for (var i = 0; i < sampleSize; i++) {
                    gauges[gaugeName(i)].redraw(newData.data[i][dataLength - 1][1], timeInteval);
                }
                startTime = newData.data[0][dataLength - 1][0];

                updateChart();
            }

            function updateChart() {
                timeoutId = $timeout(function () {
                    IOLab.getSensorData(addDataToChart, { remote: remote, sensor: sensor, startTime: startTime });
                }, timeInteval, false);
            }

            element.bind('$destroy', function () {
                $timeout.cancel(timeoutId);
            });


            scope.$on('dataStarted', function () {
                console.log('*** startData ***');
                updateChart();
            });
            scope.$on('dataStopped', function () {
                console.log('*** stopData ***');
                $timeout.cancel(timeoutId);

                for (var key in gauges) {
                    gauges[key].redraw(0, timeInteval);
                }
            });

        }
    }
});

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').factory('Player', function($q, $http, $templateCache, $document, $sce, IOLab, Logger, IOLabVersion) {

    var currentLesson = {};
    var slideShowData = {};
    var protocol = 'local://';

    function _initCurrentLesson() {
        currentLesson.protocol = protocol;
        currentLesson.pathname = null;
        currentLesson.trustedpathname = null;
        currentLesson.name = null;
        currentLesson.path = null;
        currentLesson.longName = null;
    }

    function _initSlideShowData(data) {
        var defaultData = {
            reportID: IOLab.acquisition().reportID,
            startTime: new Date(),
            slides: {}
        };

        slideShowData = data || defaultData;
    }

    function _lessonUrlParse(lessonFilePath) {
        var idx1 = _.findLastIndex(lessonFilePath, function(char) { return char == '/' }),
            idx2 = lessonFilePath.search('IOLab-WorkFiles');

        return {
            pathname: lessonFilePath,
            trustedpathname: $sce.trustAsResourceUrl(lessonFilePath),
            path: lessonFilePath.substring(0, idx1 + 1),
            name: lessonFilePath.substring(idx1 + 1, lessonFilePath.length),
            longName: lessonFilePath.substring(idx2, lessonFilePath.length)
        };
    }

    function _setCurrentLesson(lessonFilePath) {
        _initCurrentLesson();

        if (! lessonFilePath) {
            document.title = IOLabVersion.appTitle;
            return;
        }

        _.extend(currentLesson, _lessonUrlParse(lessonFilePath));

        document.title = IOLabVersion.appTitle + ' — ' + currentLesson.longName;
    }

    function setLessonTemplate(filename, slideShowData) {
        // Reset the current lesson if present
        $templateCache.remove(currentLesson.pathname);
        _setCurrentLesson();

        if (! filename) {
            return $q.when();
        }

        // Load new lesson template
        var deferred = $q.defer();

        $http.get(protocol + filename)
            .then(function (result) {

                _setCurrentLesson(filename);
                _initSlideShowData(slideShowData);

                // Replace "src" by "player-src"
                // Replace "href" by "player-href"
                var template = result.data
                    .replace(/ src=/g, ' player-src=')
                    .replace(/ href=/g, ' player-href=');

                $templateCache.put(filename, template);
                deferred.resolve();
            })
            .catch(function(err){
                console.error('Error happened while parsing lesson template : ', err);
            });

        return deferred.promise;
    }

    function lessonFileParser(filename) {

        var deferred = $q.defer(),
            lessonMetaData = {
                file: _lessonUrlParse(filename),
                slideShow: {}
            };

        $http.get(protocol + filename)
            .then(function (result) {
                if (!result.data) {
                    deferred.reject('Cannot find file ' + filename);
                }

                var parser = new DOMParser(),
                    htmlLesson = parser.parseFromString(result.data, "text/html"),
                    slideShow = htmlLesson ? htmlLesson.getElementsByTagName('slide-show') : undefined,
                    message;

                if (!slideShow || slideShow.length === 0) {
                    message = '*** Player — No <slide-show> element in file: ' + filename;
                    Logger.info(message);
                    return deferred.resolve({ error: message });
                }

                var attrs = slideShow[0].attributes;

                lessonMetaData.slideShow.id = _.get(attrs, 'id.value');
                lessonMetaData.slideShow.author = _.get(attrs, 'author.value');
                lessonMetaData.slideShow.description = _.get(attrs, 'description.value');

                deferred.resolve(lessonMetaData);
            })
            .catch(function(err){
                console.error('Error happened while parsing lesson file : ', err);
            });

        return deferred.promise;
    }

    _initCurrentLesson();

    return {
        getSlideShowData: function() {return slideShowData; },
        currentLesson: currentLesson,

        lessonFileParser: lessonFileParser,
        setLessonTemplate: setLessonTemplate
    };
});

angular.module('IOLabApp').directive('player', function () {
    'use strict';
    return {
        restrict: 'E',
        template:
            '<div style="margin: 70px 5px 25px 5px;">' +
            '   <ng-include src="Player.currentLesson.trustedpathname" />' +
            '</div>',
        controller: function($scope, Player) {
            $scope.Player = Player;
        }
    };
});

angular.module('IOLabApp').factory('slideShowService', function() {
    "use strict";
    return {};
});

//
// Player Slide show
//

angular.module('IOLabApp').directive('slideShow', function () {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        controller: 'slideShowCtrl',
        template:
            '<div class="slide-show">' +
            '   <div class="summary-container">' +
            '       <span class="title">Slide:</span>' +
            '       <summary ng-repeat="slide in slideShowCtrl.slideShowData.slides"></summary>' +
            '   </div>' +
            '   <hr style="margin-top: 8px; margin-bottom: 10px;"/>' +
            '   <div ng-if="!slideShowCtrl.slideShowData.reportID" class="error">' +
            '       ERROR - No reportID defined' +
            '   </div>' +
            '   <div ng-if="!slideShowCtrl.slideShowData.slideShow.id" class="error">' +
            '       ERROR - The attribute \"id\" of tag &lt;slide-show&gt; is missing' +
            '   </div>' +
            '   <div ng-transclude></div>' +
            '</div>',


        link: function (scope, element, attrs) {
            //console.log('slideShowLink');

            // Save slide meta data info
            // These info will be used in the resource manager
            scope.slideShowCtrl.slideShowData.slideShow = {
                id: attrs.id,
                author: attrs.author,
                description: attrs.description
            };

            /*
             * The link function of the top most directive is executed
             * after execution of all children directive controller/link
             * functions
             */

            // Display first slide here.
            scope.slideShowCtrl.nextSlide();
        }
    };
});

angular.module('IOLabApp').controller('slideShowCtrl', function ($scope, $q, $timeout, IOLab, Player, mgtCMD,
                                                                 slideShowService, configService, snapshotService) {
        'use strict';
        console.log('slideShowCtrl');

        slideShowService.slideShowCtrl = this;
        $scope.slideShowCtrl = this;

        var slideShowCtrl = this;
        slideShowCtrl.lastSlideIdx = -1;
        slideShowCtrl.currentSlideIdx = -1;
        slideShowCtrl.numberOfSlide = 0;
        slideShowCtrl.remoteAlert = false;

        slideShowCtrl.slideShowData = Player.getSlideShowData();

        // Determine the lastSlideIdx in case of reload,
        // and set the acquisitionSet, snapshot and paramPlotList based
        // on the before last slide
        var lastSavedSlideData = {};
        _(slideShowCtrl.slideShowData.slides)
            .map().sortBy('slideKey')
            .forEach(function(slideData, idx) {
                if (slideData.savedOn) {
                    slideShowCtrl.lastSlideIdx = idx + 1;
                    lastSavedSlideData = angular.copy(slideData);
                } else
                if  (idx === slideShowCtrl.lastSlideIdx) {
                    // Copy the acquisitionSet and snapshot of the last saved slide
                    // to the last available slideData
                    slideData.acquisitionSet = lastSavedSlideData.acquisitionSet || { fixedConfig: {}, runs: [] };
                    slideData.snapshot = lastSavedSlideData.snapshot || {};

                    if (lastSavedSlideData.paramPlotList) {
                        slideData.paramPlotList = lastSavedSlideData.paramPlotList;
                    }
                }
            })
            .value();

        //
        // Scope variable exposed to all slides view!
        //
        slideShowCtrl.chartType = { value: 'chart', newValue: 'chart' };

        // List of sensors, implies creation of a chart directive
        slideShowCtrl.chartList = [];
        slideShowCtrl.paramPlotList = [];

        var chartScopes = [],
            paramPlotScopes = [],
            slideScopes = [];

        //
        // Chart management
        //

        function findChartScope(remote_, sensor_, coordinate_) {
            var remote = parseInt(remote_),
                sensor = parseInt(sensor_),
                coordinate = coordinate_ != undefined ? parseInt(coordinate_) : undefined;

            return _.find(chartScopes, function (scope) {
                return scope.sensor.sensorType == sensor
                    && scope.sensor.remote == remote
                    && (coordinate == undefined || scope.sensor.coordinate == coordinate);
            });
        }

        // Wait that all charts are created before restoring the snapshot
        // Typically when switching from param plot mode to chart mode
        function restoreSnapshot (slideIdx) {
            var snapshot = slideScopes[slideIdx].slideCtrl.slideData.snapshot;
            var promises = [];

            _.forEach((snapshot || {}).sensors, function(snSensor) {
                var deferred = $q.defer();
                promises.push(deferred.promise);
                var cancel = $scope.$on('chartScope:created/hidden', function(event, chartScope) {
                    if (chartScope.isSameSensor(snSensor)) {
                        deferred.resolve();
                        cancel();
                    }
                });
            });

            return $q.all(promises)
                .then(function() {
                    snapshotService.restoreSnapshot(snapshot);
                });
        }

        slideShowCtrl.registerChart = function (chartScope) {
            chartScopes.push(chartScope);
            $scope.$broadcast('chartScope:created/hidden', chartScope);
        };

        slideShowCtrl.unregisterChart = function (chartScope) {
            _.remove(chartScopes, chartScope);
        };

        slideShowCtrl.registerParamPlot = function (paramPlotScope) {
            paramPlotScopes.push(paramPlotScope);
            $scope.$broadcast('paramPlotScope:created/hidden', paramPlotScope);
        };

        slideShowCtrl.unregisterParamPlot = function (paramPlotScope) {
            _.remove(paramPlotScopes, paramPlotScope);
        };
        
        slideShowCtrl.resetCharts = function(snapshot) {
            if (!snapshot) { return slideShowCtrl.resetAllCharts(); }

            _.remove(slideShowCtrl.chartList, function(plSensor) {
                var sensor = _.find((snapshot || {}).sensors, function(nsSensor) {
                    return nsSensor.remote == plSensor.remote &&
                        nsSensor.sensor == plSensor.sensorType &&
                        nsSensor.coordinate == plSensor.coordinate;
                });
                return !sensor;
            });
            _.remove(chartScopes, function(scope) {
                var sensor = _.find((snapshot || {}).sensors, function(nsSensor) {
                    return nsSensor.remote == scope.sensor.remote &&
                        nsSensor.sensor == scope.sensor.sensorType &&
                        nsSensor.coordinate == scope.sensor.coordinate;
                });
                return !sensor;
            });
        };

        slideShowCtrl.resetAllCharts = function() {
            // Used when reset acquisition,
            // but also when reload slides in some cases
            slideShowCtrl.chartList = [];
            chartScopes = [];
        };

        slideShowCtrl.resetAllParamPlots = function() {
            // Used when reset acquisition
            slideShowCtrl.paramPlotList = [];
            paramPlotScopes = [];
        };

        slideShowCtrl.showChart = function (remote, sensorType, coordinate) {

            var chartScope = findChartScope(remote, sensorType, coordinate);
            if (chartScope) {
                chartScope.showChart = true;
                $scope.$broadcast('chartScope:created/hidden', chartScope);
                return;
            }
            
            var query = {remote: remote, sensorType: sensorType};
            var acqSensor = _.find(IOLab.acquisition(remote).sensors, query);

            if (coordinate != undefined) {
                query.coordinate = coordinate;
            }
            var plSensor = _.find(slideShowCtrl.chartList, query);

            // Create plSensor if not yet present
            if (acqSensor && !plSensor) {
                var param = configService.getSensor(sensorType);
                acqSensor.desc = param.desc;
                acqSensor.shortDesc = param.shortDesc;

                if (coordinate != undefined) {
                    // Make a copy of acqSensor if chart is split
                    acqSensor = angular.copy(acqSensor);

                    acqSensor.subDesc = (param.subCharts[coordinate] || {}).desc;
                    acqSensor.coordinate = coordinate;
                }

                slideShowCtrl.chartList.push(acqSensor);
            }
        };

        slideShowCtrl.hideChart = function (remote, sensorType, coordinate) {
            var chartScope = findChartScope(remote, sensorType, coordinate);
            if (chartScope) {
                chartScope.showChart = false;
                $scope.$broadcast('chartScope:created/hidden', chartScope);
            }
        };

        slideShowCtrl.removeChart = function(remote, sensorType, coordinate) {
            _.remove(slideShowCtrl.chartList,
                c => c.remote == remote && c.sensorType == sensorType && c.coordinate == coordinate);
        };

        slideShowCtrl.showParamPlot = function (remote, sensorType1, coordinate1, sensorType2, coordinate2) {

            var acqSensor1 = _.find(IOLab.acquisition(remote).sensors, {
                remote: remote,
                sensorType: sensorType1
            });
            var acqSensor2 = _.find(IOLab.acquisition(remote).sensors, {
                remote: remote,
                sensorType: sensorType2
            });

            var query = {
                remote: remote,
                sensor1: { sensorType : sensorType1 },
                sensor2: { sensorType : sensorType2 }
            };

            if (coordinate1 != undefined) { query.sensor1.coordinate = coordinate1; }
            if (coordinate2 != undefined) { query.sensor2.coordinate = coordinate2; }

            var plParamPlot = _.find(slideShowCtrl.paramPlotList, query);

            // Create plSensor if not yet present
            if (acqSensor1 && acqSensor2 && !plParamPlot) {
                // First Sensor
                var param1 = configService.getSensor(sensorType1);
                acqSensor1.desc = param1.desc;
                acqSensor1.shortDesc = param1.shortDesc;
                if (coordinate1 != undefined) {
                    acqSensor1 = angular.copy(acqSensor1);
                    acqSensor1.subDesc = (param1.subCharts[coordinate1] || {}).desc;
                    acqSensor1.coordinate = coordinate1;
                    acqSensor1.sampleSize = 1;
                }

                // Second Sensor
                var param2 = configService.getSensor(sensorType2);
                acqSensor2.desc = param2.desc;
                acqSensor2.shortDesc = param2.shortDesc;
                if (coordinate2 != undefined) {
                    acqSensor2 = angular.copy(acqSensor2);
                    acqSensor2.subDesc = (param2.subCharts[coordinate2] || {}).desc;
                    acqSensor2.coordinate = coordinate2;
                    acqSensor2.sampleSize = 1;
                }

                slideShowCtrl.paramPlotList.push({
                    remote: remote,
                    sensor1: acqSensor1,
                    sensor2: acqSensor2
                });
            }
        };

        slideShowCtrl.showParamPlotControls = function() {
            return (slideShowCtrl.paramPlotList || []).length  || slideShowCtrl.showSensorSelector;
        };

        slideShowCtrl.showTrace = function (remote, sensor, traceIdx, value) {
            var chartScope = findChartScope(remote, sensor);
            if (chartScope && chartScope.control.traceSettings[traceIdx]) {
                chartScope.control.traceSettings[traceIdx].show = value;
            }
        };
        
        slideShowCtrl.reverseYAxis = function (remote, sensor, coordinate, value) {
            var chartScope = findChartScope(remote, sensor, coordinate);
            if (chartScope) {
                chartScope.control.reverseAxis = value;
            }
        };

        slideShowCtrl.traceSmoothing = function (remote, sensor, coordinate, value) {
            var chartScope = findChartScope(remote, sensor, coordinate);
            if (chartScope) {
                chartScope.control.timeAverage = value;
            }
        };

        slideShowCtrl.setYAxisRange = function (remote, sensor, coordinate, range) {
            var chartScope = findChartScope(remote, sensor, coordinate);
            if (chartScope && range.length == 2) {
                chartScope.chartCtrl.chart.scale({y0: range[0], y1: range[1]}, { setDefScale: true });
                chartScope.chartCtrl.chart();
            }
        };

        slideShowCtrl.resetChartList = function() {
            _.forEach(chartScopes, function(chartScope) {
                chartScope.chartCtrl.chart.reset();
            });
        };

        slideShowCtrl.setAcquisitionDuration = function (duration) {
            IOLab.acquisitionSet().duration = duration || undefined;

            if (!duration) {
                return;
            }

            _.forEach(chartScopes, function(chartScope) {
                chartScope.chartCtrl.chart.scale({x0: 0, x1: duration}, { setDefScale: true });
                chartScope.chartCtrl.chart();
            });
        };

        slideShowCtrl.getSvgHeight = function() {
            var defaultChartHeight = 250,
                headerHeight = slideShowCtrl.chartContainerOffset(),
                availableHeight = window.innerHeight - headerHeight,
                totalNumberOfChart = _.filter(chartScopes, { showChart: true }).length;

            if (totalNumberOfChart == 0) {
                return defaultChartHeight;
            }

            var svgHeight = availableHeight / totalNumberOfChart - 40;

            // Maximal height
            if (svgHeight > defaultChartHeight) {
                return defaultChartHeight;
            }
            // Minimal height: 120
            return Math.max(svgHeight, 120);
        };

        //
        // Slide management
        //

        // We can store in the slide scope some information
        // as the fact that a snapshot has been done (permanent info)
        slideShowCtrl.registerSlide = function (slideScope) {
            slideScopes.push(slideScope);
            slideShowCtrl.numberOfSlide += 1;
        };

        slideShowCtrl.nextSlide = function () {
            var nextSlideData;

            function getSlideData (slideIdx) {
                return _.get(slideScopes[slideIdx], 'slideCtrl.slideData');
            }

            function goNextSlide () {
                slideShowCtrl.currentSlideIdx += 1;
                slideScopes[slideShowCtrl.currentSlideIdx].showSlide = true;

                // Set last slideIdx
                slideShowCtrl.lastSlideIdx = Math.max(slideShowCtrl.currentSlideIdx, slideShowCtrl.lastSlideIdx );

                nextSlideData = getSlideData(slideShowCtrl.currentSlideIdx);
                if (! nextSlideData.startTime) {
                    nextSlideData.startTime = new Date();
                }

                // Only called when reloading a report and accessing the first slide
                if (nextSlideData.savedOn) {
                    slideShowCtrl.selectSlide(slideShowCtrl.currentSlideIdx);
                }
            }

            // Leave current slide show if not the last one
            if (slideShowCtrl.currentSlideIdx > -1 && slideShowCtrl.currentSlideIdx != slideShowCtrl.numberOfSlide - 1) {
                slideScopes[slideShowCtrl.currentSlideIdx].showSlide = false;

                // FIXME - Should be already saved => no effect ???
                slideScopes[slideShowCtrl.currentSlideIdx].saveLocalSnapshot();
                slideScopes[slideShowCtrl.currentSlideIdx].slideCtrl.savedLocally = true;
            }

            // Go to next slide ...
            if (slideShowCtrl.currentSlideIdx + 1 < slideShowCtrl.numberOfSlide) {

                if (slideShowCtrl.chartType.value == 'chart') {
                    return goNextSlide();
                }

                // Switch to chart mode
                slideShowCtrl.chartType = { value: 'chart', newValue: 'chart' };

                // Restore chart snapshot if previously in paramPlot mode
                restoreSnapshot(slideShowCtrl.currentSlideIdx)
                    .then(function() {
                        return goNextSlide();
                    })
            }
        };

        slideShowCtrl.selectSlide = function(slideIdx) {

            function selectSlide () {
                // Hide the  current slide and save the snapshot (
                slideScopes[slideShowCtrl.currentSlideIdx].showSlide = false;

                // Reload the new slide
                slideShowCtrl.currentSlideIdx = slideIdx;
                slideScopes[slideShowCtrl.currentSlideIdx].reloadSlide()
                    .then(function() {
                        slideScopes[slideShowCtrl.currentSlideIdx].showSlide = true;
                    });
            }

            slideScopes[slideShowCtrl.currentSlideIdx].saveLocalSnapshot();
            slideScopes[slideShowCtrl.currentSlideIdx].slideCtrl.savedLocally = true;

            if (slideShowCtrl.chartType.value == 'chart') {
                return selectSlide();
            }

            // Switch to chart mode
            slideShowCtrl.chartType = { value: 'chart', newValue: 'chart' };

            // Restore chart snapshot if currently in paramPlot mode
            restoreSnapshot(slideShowCtrl.currentSlideIdx)
                .then(function() {
                    $timeout(selectSlide, 50);
                });
        };

        // Save slide (Set endTime - Take a snapshot - Set the slide as saved)
        slideShowCtrl.saveSlide = function() {

            function getSlideCtrl (slideIdx) {
                return _.get(slideScopes[slideIdx], 'slideCtrl');
            }

            // Get the current slideCtrl
            var currentSlideCtrl = getSlideCtrl(slideShowCtrl.currentSlideIdx);

            if (!currentSlideCtrl) {
                return $q.when();
            }

            var currentSlideData = currentSlideCtrl.slideData,
                slideShowData = slideShowCtrl.slideShowData;

            return $q.when()
                //
                // Save snapshot data and the acquisitionSet if needed
                //
                .then(function() {

                    var acquisitionSet = IOLab.acquisitionSet() || {};
                    if ((acquisitionSet.runs || []).length == 0) {
                        return;
                    }

                    // Save a copy of the acquisition set
                    currentSlideData.acquisitionSet = angular.copy(acquisitionSet);

                    // If no snapshot (param plot displayed), keep current one if present
                    currentSlideData.snapshot = snapshotService.takeSnapshot() || currentSlideData.snapshot;

                    // Keep a copy of the paramPlot list. 
                    // We do not have yet a notion of snapshot for the param plot
                    currentSlideData.paramPlotList = angular.copy(slideShowCtrl.paramPlotList);
                })
                //
                // Copy calibration if needed
                //
                .then(function() {
                    var promiseQueue = $q.when();
                    var activeChartScopes = _.filter(chartScopes, function(scope) {
                        return scope.showChart;
                    });

                    _.forEach(activeChartScopes, function(scope) {
                        promiseQueue = promiseQueue.then(function() {
                            var calibration = {
                                sensor: scope.sensor.sensorType,
                                remoteID: IOLab.hardwareState(scope.sensor.remote).remoteID
                            };
                            return IOLab.manageCalibrations(mgtCMD.Export, calibration)
                        })
                    });

                    return promiseQueue;
                })
                //
                // Save report data
                //
                .then(function() {
                    currentSlideData.savedOn = new Date();
                    IOLab.manageReports(mgtCMD.Save, slideShowData.reportID, slideShowData);
                });
        };

        slideShowCtrl.updateSlide = function () {

            function getSlideCtrl (slideIdx) {
                return _.get(slideScopes[slideIdx], 'slideCtrl');
            }

            // Get the current slideCtrl
            var currentSlideCtrl = getSlideCtrl(slideShowCtrl.currentSlideIdx);

            if (!currentSlideCtrl) {
                return $q.when();
            }

            var currentSlideData = currentSlideCtrl.slideData,
                slideShowData = slideShowCtrl.slideShowData;

            return $q.when()
                //
                // Save report data
                //
                .then(function() {
                    currentSlideData.updatedOn = new Date();
                    IOLab.manageReports(mgtCMD.Save, slideShowData.reportID, slideShowData);
                });
        };

        // This function is executed when the user switches (not when we change
        // programmatically the value of the chartType)
        slideShowCtrl.typeChanged = function() {
            // Save local snapshot when switching from chart to paramPlot (each time).
            if (slideShowCtrl.chartType.newValue === 'paramPlot')  {
                slideScopes[slideShowCtrl.currentSlideIdx].saveLocalSnapshot();
            }

            // Restore chart snapshot if currently in paramPlot mode
            // ==>  wait event sent when chart is created
            if (slideShowCtrl.chartType.newValue === 'chart') {
                restoreSnapshot(slideShowCtrl.currentSlideIdx);
            }

            slideShowCtrl.chartType.value = slideShowCtrl.chartType.newValue;
        };

    });

angular.module('IOLabApp').directive('playerSrc', function ($sce, Player) {
    'use strict';
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            var src = attrs.playerSrc;

            if (attrs.playerSrc.indexOf('http') < 0) {
                src = Player.currentLesson.protocol
                    + Player.currentLesson.path
                    + src;
            }

            element.attr('src', src);
        }
    };
});

angular.module('IOLabApp').directive('playerHref', function ($sce, Player) {
    'use strict';
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            var href = attrs.playerHref,
                local = false;

            if (attrs.playerHref.indexOf('http') < 0) {
                href = Player.currentLesson.protocol
                    + Player.currentLesson.path
                    + href;
                local = true;
            }

            // <a> tag => open in external browser !!!
            if (element[0].nodeName == 'A' && !local) {
                element.on('click', function() {
                    require('electron').shell.openExternal(href);
                })
            }

            // Allow only local styles
            if (element[0].nodeName == 'LINK' && local) {
                element.attr('href', href);
            }
        }
    };
});

//
// Slide Summary
//

angular.module('IOLabApp').directive('summary', function () {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        require: '^slideShow',
        template:
            '<span>' +
            '   <span ng-switch="!!slide.savedOn || $index === slideShowCtrl.lastSlideIdx" ' +
            '         ng-class="{ selected: $index === slideShowCtrl.currentSlideIdx}">' +
            '       <span ng-switch-when="false" class="slide-summary disabled">' +
            '           {{$index + 1}}' +
            '       </span>' +
            '       <a href="" ng-switch-when="true" ng-click="slideShowCtrl.selectSlide($index)" class="slide-summary">' +
            '           {{$index + 1}}<span ng-if="!slide.savedOn">*</span>' +
            '       </a>' +
            '   </span>' +
            '</span>',
        link: function (scope, element, attrs, slideShowCtrl) {
            scope.slideShowCtrl = slideShowCtrl;
        }
    };
});


//
// Player Slide
//

angular.module('IOLabApp').directive('slide', function ($rootScope, $q, $timeout, utils, IOLab, configService, snapshotService) {
    'use strict';
    return {
        restrict: 'E',
        scope: true,
        replace: true,
        transclude: true,
        require: '^slideShow',
        template:
            '<div class="slide" ng-show="showSlide" ng-style="slideStyle">' +
            '    <div ng-transclude ng-if="showSlide"></div>' +
            '</div>',
        controller: function($scope) {
            $scope.slideCtrl = this;
        },
        link: function (scope, element, attrs, slideShowCtrl) {
            //console.log('slideLink');

            var slideNum = slideShowCtrl.numberOfSlide.toString(),
                slideKey = 'slide_' + ('000' + slideNum).substr(slideNum.length);

            slideShowCtrl.registerSlide(scope);

            // Create empty slideData object if not yet present
            slideShowCtrl.slideShowData.slides[slideKey] =
                slideShowCtrl.slideShowData.slides[slideKey] || {questions: {}};

            // Is question editable
            scope.slideCtrl.editableQuestions = attrs.editableQuestions;

            // Save a reference of the slideData on the slideCtrl
            scope.slideCtrl.slideData = slideShowCtrl.slideShowData.slides[slideKey];

            // Property used for snapshot
            scope.slideCtrl.slideData.slideKey = slideKey;

            scope.reloadSlide = function () {

                var apiAcqSet = IOLab.acquisitionSet(),
                    acqSet = scope.slideCtrl.slideData.acquisitionSet || { runs: [] },
                    snapshot = scope.slideCtrl.slideData.snapshot;

                // Restore the param plot list
                slideShowCtrl.paramPlotList = scope.slideCtrl.slideData.paramPlotList;

                return $q.when()
                    .then(function() {
                        var apiConfig1 = (apiAcqSet.fixedConfig || {})[1],
                            slideConfig1 = (acqSet.fixedConfig || {})[1];

                        // Slide has no fixed configuration ...
                        if ((!slideConfig1 && apiConfig1) || acqSet.runs.length == 0) {
                            slideShowCtrl.resetAllCharts();
                            return IOLab.resetAcquisition();
                        }
                    })
                    .then(function() {
                        return snapshotService.reloadAcquisitionSet(acqSet);
                    })
                    .then(function(result) {
                        // Check chart list
                        if (result.resetAllCharts) {
                            slideShowCtrl.resetAllCharts();
                        } else {
                            slideShowCtrl.resetCharts(snapshot);
                        }

                        // Update chart list
                        var promises = [];
                        _.forEach((snapshot || {}).sensors, function(snSensor) {
                            var remote = snSensor.remote,
                                sensorType = snSensor.sensor,
                                coordinate = snSensor.coordinate,
                                deferred;

                            // WARNING - See "slideShowCtrl.showChart"

                            var query = {remote: remote, sensorType: sensorType};
                            var acqSensor = _.find(IOLab.acquisition(remote).sensors, query);

                            if (coordinate != undefined) {
                                query.coordinate = coordinate;
                            }
                            var plSensor = _.find(slideShowCtrl.chartList, query);

                            if (acqSensor && !plSensor) {
                                var param = configService.getSensor(sensorType);
                                acqSensor.desc = param.desc;
                                acqSensor.shortDesc = param.shortDesc;

                                if (coordinate != undefined) {
                                    // Make a copy of acqSensor if chart is split
                                    acqSensor = angular.copy(acqSensor);
                                    acqSensor.subDesc = (param.subCharts[coordinate] || {}).desc;
                                    acqSensor.coordinate = coordinate;
                                }

                                slideShowCtrl.chartList.push(acqSensor);

                                deferred = $q.defer();
                                promises.push(deferred.promise);
                                var cancel = scope.$on('chartScope:created/hidden', function(event, chartScope) {
                                    if (chartScope.isSameSensor(snSensor)) {
                                        deferred.resolve();
                                        cancel();
                                    }
                                });
                            }
                        });

                        return $q.all(promises);
                    })
                    .then(function() {
                        snapshotService.restoreSnapshot(snapshot);

                        // Must be done after the call to "snapshotService.restoreSnapshot()"
                        // since this call is responsible to show the chart
                        $rootScope.$broadcast('recompute-chart-height');
                    })
                    .then(utils.wait(150));
            };

            scope.saveLocalSnapshot = function() {
                var slideSaved = !!scope.slideCtrl.slideData.savedOn;

                if (!slideSaved) {
                    scope.slideCtrl.slideData.acquisitionSet = angular.copy(IOLab.acquisitionSet());
                    scope.slideCtrl.slideData.snapshot = snapshotService.takeSnapshot();
                    scope.slideCtrl.slideData.paramPlotList = slideShowCtrl.paramPlotList;
                }
            };

            //
            // Manage slide scroll
            //
            function setSlideStyle () {
                $timeout(()=> {
                    var slideTop = element[0].documentOffsetTop,
                        winHeight = window.innerHeight - 5,
                        winWidth = window.innerWidth,
                        scroll = winWidth >= 768 &&
                            (slideShowCtrl.scrollableSlides || scope.slideCtrl.scrollable);

                    scope.slideStyle = !scroll ? {} : { 'max-height': (winHeight - slideTop) + 'px' };
                });
            }

            $rootScope.$on('recompute-chart-height', setSlideStyle);
            setSlideStyle();
        }
    };
});

angular.module('IOLabApp').directive('scrollableSlides', function() {
    return {
        restrict: 'AC',
        require: 'slideShow',
        link: function(scope, element, attrs, ctrl) {
            ctrl.scrollableSlides = true;
        }
    };
});

angular.module('IOLabApp').directive('scrollable', function() {
    return {
        restrict: 'AC',
        require: 'slide',
        link: function(scope, element, attrs, ctrl) {
            ctrl.scrollable = true;
        }
    };
});

//
// Player Chart
//

angular.module('IOLabApp').directive('chartContainer', function () {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        require: '^slideShow',
        template:
            '<div class="data-container">' +

            '    <!-- Alert when no remote connected -->' +
            '    <div ng-if="slideShowCtrl.remoteAlert">' +
            '        <div class="alert alert-danger" style="width: 100%; text-align: center; margin-bottom: 10px;">' +
            '            <strong ng-if="slideShowCtrl.remoteAlert.message">{{slideShowCtrl.remoteAlert.message}}</strong>' +
            '            <strong ng-if="!slideShowCtrl.remoteAlert.message">No remote detected. Check if the dongle is plugged and if the remote is powered on</strong>' +
            '        </div>'+
            '    </div>' +

            '    <!-- Sensor Selector -->' +
            '    <div ng-if="slideShowCtrl.showSensorSelector">' +
            '       <section class="three-columns">' +
            '           <sensor-selector remote="1"></sensor-selector>' +
            '       </section>' +
            '   </div>' +

            '    <!-- List of runs -->' +
            '    <div class="acquisition-set-container" ng-if="slideShowCtrl.chartList.length || IOLab.acquisitionSet().runs.length">' +
            '        <h4>' +
            '            <span style="cursor: pointer;" ng-click="isOpen.value = !isOpen.value;">Acquisition Sets</span>' +
            '            <small ng-if="IOLab.acquisitionSet().duration" title="Fixed acquisition duration">(Fixed duration {{IOLab.acquisitionSet().duration}} sec)</small>' +
            '        </h4>' +
            '        <div collapse="!isOpen.value" ng-repeat="run in IOLab.acquisitionSet().runs" style="margin: 10px 0;">' +
            '            <span>{{run.name}}</span> ' +
            '            (<small style="color: grey;">ID: {{run.acqID}}</small>)' +
            '            <a href="#"  ng-if="!run.readOnly" ng-click="IOLab.removeAcquisitionSetRun(run)">[Remove]</a></span> ' +
            '        </div>' +
            '        <div ng-if="IOLab.acquisitionSet().runs.length == 0" style="margin: 10px 0;">' +
            '            Click on "Record" to start a new acquisition.' +
            '        </div>' +
            '    </div>' +

            '    <!-- Chart and parametric plot list -->' +
            '   <div chart-container-offset>' +
            '       <div ng-if="slideShowCtrl.chartType.value == \'chart\'">' +
            '           <iolab-chart ng-repeat="sensor in slideShowCtrl.chartList" ng-show="showChart"></iolab-chart>' +
            '       </div>' +
            '       <div ng-if="slideShowCtrl.chartType.value == \'paramPlot\'">' +
            '           <iolab-parametric-plot ng-repeat="plotParams in slideShowCtrl.paramPlotList"></iolab-parametric-plot>' +
            '       </div>' +
            '   </div>' +
            '</div>',
        controller: function($scope){
            $scope.isOpen = { value: true };
        }
    };
});

angular.module('IOLabApp').directive('chartContainerOffset', function() {
    return{
        require: ['^slideShow'],
        link: function(scope, element, attrs, ctrls){

            ctrls[0].chartContainerOffset = function() {
                return element[0].documentOffsetTop;
            };
        }
    };
});

//
// Player Questions
//

angular.module('IOLabApp').directive('question', function () {
    'use strict';
    return {
        restrict: 'E',
        scope: true,
        replace: true,
        transclude: true,
        require: ['^slideShow', '^slide'],
        template:
            '<div class="question-container" ng-class="{\'question-editable\' : slideCtrl.slideData.savedOn && questionCtrl.editable, editing: questionCtrl.questionEditedData}">' +
            '   <span ng-if="!questionCtrl.questionID" class="error">' +
            '       ERROR - The attribute \"id\" of tag &lt;question&gt; is missing' +
            '   </span>' +
            '   <div ng-transclude></div>' +
            '   <div ng-if="questionCtrl.freeText">' +

            '       <!-- Initial TextArea allowing the user to enter the first question answer -->' +
            '       <!-- Span allowing the user to view the current question answer and edit button-->' +
            '       <div ng-if="!questionCtrl.questionEditedData">' +
            '           <textarea ng-model="questionCtrl.questionData.value" ng-disabled="slideCtrl.slideData.savedOn"></textarea>' +
            '       </div>' +

            '       <!-- TextArea and control buttons allowing the user to edit the current question answer-->' +
            '       <div ng-if="questionCtrl.questionEditedData && slideCtrl.slideData.savedOn">'+
            '           <textarea ng-model="questionCtrl.questionEditedData.value"></textarea>' +
            '       </div>'+

            '   </div>' +

            '   <!-- Edit button allowing the user to edit the question answer --> ' +
            '   <!-- Control buttons allowing the user to save the current question answer or cancel the edition-->' +
            '   <div class="question-edit-control" ng-if="slideCtrl.slideData.savedOn && questionCtrl.editable">' +
            '       <a ng-if="!questionCtrl.questionEditedData" ng-click="edit()"><small>[Edit]</small></a>' +
            '       <a ng-if="questionCtrl.questionEditedData" ng-click="confirmEdition()"><small>[Save]</small></a>' +
            '       <a ng-if="questionCtrl.questionEditedData" ng-click="cancelEdition()"><small>[Cancel]</small></a>' +
            '   </div>' +

            '</div>',
        controller: function($scope) {

            $scope.questionCtrl = this;

            //  Edition management
            $scope.edit = function (){
                $scope.questionCtrl.questionEditedData = angular.copy($scope.questionCtrl.questionData);
            };

            $scope.confirmEdition = function (){

                //  Save the initial question data in case of an error
                var initialData = angular.copy($scope.questionCtrl.questionData);

                //  Update questionEditedData object and try to save it. If an error should occur the edition will be cancelled
                angular.copy($scope.questionCtrl.questionEditedData, $scope.questionCtrl.questionData);

                $scope.slideShowCtrl.updateSlide()
                    .catch(function(){
                        //  Something went wrong => reset question data
                        angular.copy(initialData, $scope.questionCtrl.questionData);
                    })
                    .finally(function(){
                        $scope.questionCtrl.questionEditedData = undefined;
                    });
            };

            $scope.cancelEdition = function (){
                $scope.questionCtrl.questionEditedData = undefined;
            };
        },
        link: function(scope, element, attrs, ctrls) {
            var slideShowCtrl = ctrls[0],
                slideCtrl = ctrls[1];

            scope.slideShowCtrl = slideShowCtrl;
            scope.slideCtrl = slideCtrl;

            // Export to lower directives
            scope.questionCtrl.questionID = attrs.id;
            scope.questionCtrl.freeText = attrs.hasOwnProperty('freeText');
            scope.questionCtrl.checkBox = attrs.hasOwnProperty('checkBox');
            scope.questionCtrl.radioButton = attrs.hasOwnProperty('radioButton');

            if (slideCtrl.editableQuestions === 'true') {
                scope.questionCtrl.editable = !(attrs.editable === 'false');
            } else {
                scope.questionCtrl.editable = attrs.editable === 'true';
            }

            if (attrs.id) {
                // Create empty questionData object if not yet present
                slideCtrl.slideData.questions[attrs.id] = slideCtrl.slideData.questions[attrs.id] || {};
                scope.questionCtrl.questionData = slideCtrl.slideData.questions[attrs.id];
            } else {
                scope.questionCtrl.questionData = {};
            }

            scope.questionCtrl.questionEditedData = undefined;
        }
    };
});

angular.module('IOLabApp').directive('content', function () {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        template: '<label ng-transclude class="question-content-container"></label>'
    };
});

angular.module('IOLabApp').directive('item', function () {
    'use strict';
    return {
        restrict: 'E',
        scope: true,
        replace: true,
        transclude: true,
        require: ['^slide', '^question'],
        template:
            '<div class="question-item-container">' +
            '   <label>' +
            '       <input ng-if="questionCtrl.radioButton && !questionCtrl.questionEditedData" type="radio" name="{{questionCtrl.questionID}}" ng-model="questionCtrl.questionData.value" ng-value="mcValue" ng-disabled="slideCtrl.slideData.savedOn" >' +
            '       <input ng-if="questionCtrl.radioButton && questionCtrl.questionEditedData" type="radio" name="{{questionCtrl.questionID}}" ng-model="questionCtrl.questionEditedData.value" ng-value="mcValue">' +
            '       <input ng-if="questionCtrl.checkBox && !questionCtrl.questionEditedData" type="checkbox" name="{{questionCtrl.questionID}}" ng-model="questionCtrl.questionData.values[mcValue]" ng-disabled="slideCtrl.slideData.savedOn" >' +
            '       <input ng-if="questionCtrl.checkBox && questionCtrl.questionEditedData" type="checkbox" name="{{questionCtrl.questionID}}" ng-model="questionCtrl.questionEditedData.values[mcValue]" >' +
            '       &nbsp;<span ng-transclude></span>' +
            '   </label>' +
            '   <span ng-if="!mcValue" class="error">' +
            '       ERROR - The attribute \"value\" of tag &lt;item&gt; is missing' +
            '   </span>' +
            '</div>',
        link: function(scope, element, attrs, ctrls) {
            scope.slideCtrl = ctrls[0];
            scope.questionCtrl = ctrls[1];

            scope.mcValue = attrs.value;
        }
    };
});

//
//
//
angular.module('IOLabApp').directive('showSensorSelector', function (IOLab) {
    "use strict";

    return {
        restrict: 'AC',
        require: ['^slideShow'],
        link: function (scope, element, attrs, ctrl) {

            console.log('playerSensorSelector');

            var slideShowCtrl = ctrl[0];

            slideShowCtrl.showSensorSelector = true;

            scope.$on('chart-list-changed', function(event, args) {

                //
                // Update chart list
                //
                if (args.reset) {
                    slideShowCtrl.resetAllCharts();
                }

                _.forEach(args.add, function(s) {
                    slideShowCtrl.showChart(s.remote, s.sensorType, s.coordinate)
                });

                _.forEach(args.remove, function(s) {
                    slideShowCtrl.removeChart(s.remote, s.sensorType, s.coordinate)
                });

                //
                // Update param plot list (this list is build from the chart list in the chartListService)
                //
                if (slideShowCtrl.chartList.length > 0) {
                    slideShowCtrl.paramPlotList = [{
                        remote: args.remote,
                        config: IOLab.acquisition(args.remote).config,
                        sensor1: slideShowCtrl.chartList[0],
                        sensor2: slideShowCtrl.chartList[1] || slideShowCtrl.chartList[0]
                    }];
                } else {
                    slideShowCtrl.paramPlotList = []
                }
            });
        }
    };
});


//
// Player code
//

angular.module('IOLabApp').directive('codeBlock', function () {
    "use strict";

    return {
        restrict: 'E',
        transclude: true,
        require: ['^slideShow', '^slide'],
        template:
            '<div ng-if="!slideCtrl.slideData.savedOn && !slideCtrl.savedLocally" ' +
            '     ng-transclude>' +
            '</div>',
        controller: function ($scope, $q) {
            var ctrl = this;
            var promiseQueue = $q.when();
            var promiseList = [];

            console.log('codeBlockCtrl');

            ctrl.addPromise = function() {
                var args = [].slice.call(arguments);

                promiseQueue = promiseQueue.then(function () {
                    var fct = args.shift();

                    // Execute the function and return the promise
                    var result = fct.apply(null, args);
                    if (result.deferred) {
                        promiseList.push(result.deferred);
                        return result.deferred.promise;
                    }
                    return result;
                });
            };

            $scope.$on('$destroy', function () {
                console.log('*** scope.$on => destroy codeBlock ***', promiseList);
                _.forEach(promiseList, function(promise) {
                    promise.reject();
                });

                $scope.slideShowCtrl.remoteAlert = false;
                $scope.slideShowCtrl.showSensorSelector = false;
            });
        },
        link: function(scope, element, attrs, ctrls) {
            scope.slideShowCtrl = ctrls[0];
            scope.slideCtrl = ctrls[1];
        }
    };
});

angular.module('IOLabApp').directive('code', function () {
    "use strict";
    return {
        restrict: 'E',
        replace: true,
        template: '<div></div>'
    };
});

angular.module('IOLabApp').directive('playerResetAcquisition', function ($q, $timeout, utils, IOLab) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {

            console.log('playerResetAcquisitionLink');

            var slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            function resetAcquisition () {
                slideShowCtrl.resetAllCharts();
                slideShowCtrl.resetAllParamPlots();
                return IOLab.resetAcquisition()
                    .then(utils.wait(150));
            }

            // Return promise
            codeCtrl.addPromise(resetAcquisition);
        }
    };
});

angular.module('IOLabApp').directive('playerResetChartList', function ($q, $timeout, utils) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {

            console.log('playerResetChartListLink');

            var slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            function resetChartList () {
                return $q.when(slideShowCtrl.resetChartList())
                    .then(utils.wait(150));
            }

            // Return promise
            codeCtrl.addPromise(resetChartList);
        }
    };
});

angular.module('IOLabApp').directive('playerSetFixedConfiguration', function ($q, $timeout, IOLab) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {

            var config = attrs.playerSetFixedConfiguration * 1,
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            console.log('playerSetFixedConfigurationLink');

            // Return promise
            function setFixedConfig() {

                var deferred = $q.defer();
                var listener = scope.$watch(
                    function () { return IOLab.hardwareState(remote).rfStatus; },
                    function (rfStatus) {

                        if (rfStatus === 0) {
                            slideShowCtrl.remoteAlert = {
                                message: "The remote " + remote +  " is not detected, check if the dongle is plugged and the remote powered on"
                            };
                            return;
                        }
                        listener();

                        slideShowCtrl.remoteAlert = false;

                        if (IOLab.hardwareState(remote).fixedConfig == config) {
                            return deferred.resolve();
                        }

                        return IOLab.setFixedConfig(remote, config)
                            .then(function () {
                                var cancel = scope.$on('hardwareStateChanged', function () {
                                    cancel();
                                    deferred.resolve();
                                });
                            });
                    });
                return deferred.promise;
            }

            codeCtrl.addPromise(setFixedConfig);
        }
    };
});

angular.module('IOLabApp').directive('playerLoadAcquisition', function ($q, $timeout, IOLab, Player, Logger, mgtCMD) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {

            console.log('playerLoadAcquisitionLink');

            var slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1],

                acqSrc = attrs.playerLoadAcquisition;


            var split = acqSrc.split('/'),
                acqID = split.pop(),
                acqPath = split.join('/');

            function loadAcquisition () {

                var reportID = (IOLab.acquisition() || {}).reportID,
                    activityPath = Player.currentLesson.path + acqPath;

                return IOLab.copyAcquisitionToReport(activityPath, acqID, reportID)
                    .then(function(result) {
                        // FIXME - Manage the error
                        if (!result.success) {
                            Logger.info('ERR', result.message);
                            return $q.when();
                        }

                        var deferred = $q.defer();

                        // Load acquisition
                        IOLab.manageAcquisitions(mgtCMD.Reload, acqID)
                            .then(function() {
                                // Acquisition reload send hardwareStateChanged and acquisitionStackChanged
                                // => Wait the event before executing the new code block command
                                var cancel = scope.$on('acquisitionStackChanged', function(event, args) {
                                    cancel();

                                    // Push the new run in the acquisitionSet
                                    // This run is a template => Readonly
                                    IOLab.pushAcquisitionSetRun(acqID, { template: true });
                                    deferred.resolve();
                                });
                            });

                        return deferred.promise;
                    })
            }

            // Return promise
            codeCtrl.addPromise(loadAcquisition);
        }
    };
});

angular.module('IOLabApp').directive('playerShowChartSensor', function ($rootScope, $q, configService) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerShowChartLink');

            var sensors = attrs.playerShowChartSensor.split(' '),
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            _.forEach(sensors, function(sensor) {
                var sensorType, action, split, coordinates;

                if (sensor[0] == '!') {
                    action = 'hideChart';
                    sensor = sensor.slice(1);
                }
                else {
                    action = 'showChart';
                }

                split = sensor.split('#');
                sensorType = parseInt(split[0]);
                coordinates = split[1];

                var param = configService.getSensor(sensorType);
                if (!coordinates && param.subCharts) {
                    coordinates = _.range(1, param.subCharts.length + 1); // [1, 2, 3]
                }

                function addPromise (coord_) {
                    var coord = coord_ != undefined ? parseInt(coord_) -1 : undefined; // NaN

                    codeCtrl.addPromise(function () {
                        var deferred = $q.defer();
                        var cancel = scope.$on('chartScope:created/hidden', function(event, chartScope) {
                            if (chartScope.isSameSensor({ remote: remote, sensor: sensorType, coordinate: coord })) {
                                $rootScope.$broadcast('recompute-chart-height'); // Recompute height
                                deferred.resolve();
                                cancel();
                            }
                        });

                        slideShowCtrl[action](remote, sensorType, coord);
                        return deferred.promise;
                    });
                }

                if (!coordinates || coordinates.length == 0) {
                    addPromise();
                } else {
                    _.forEach(coordinates, addPromise);
                }

            });
        }
    };
});

angular.module('IOLabApp').directive('playerSetupParamPlot', function ($rootScope, $q) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerSetUpParamPlot');

            var sensor1 = attrs.sensorX,
                sensor2 = attrs.sensorY,
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            var split1 = sensor1.split('#'),
                sensorType1 = parseInt(split1[0]),
                coordinate1 = split1[1] != undefined ? parseInt(split1[1]) -1 : undefined;
            
            var split2 = sensor2.split('#'),
                sensorType2 = parseInt(split2[0]),
                coordinate2 = split2[1] != undefined ? parseInt(split2[1]) -1 : undefined;
            
            codeCtrl.addPromise(function() {
                return $q.when(slideShowCtrl.showParamPlot(remote, sensorType1, coordinate1, sensorType2, coordinate2));
            });
        }
    };
});

angular.module('IOLabApp').directive('playerHideChartSensor', function ($rootScope, $q, configService) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerHideChartSensorLink');

            var sensors = attrs.playerHideChartSensor.split(' '),
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            _.forEach(sensors, function(sensor) {
                var sensorType, action, split, coordinates;

                if (sensor[0] == '!') {
                    action = 'showChart';
                    sensor = sensor.slice(1);
                }
                else {
                    action = 'hideChart';
                }

                split = sensor.split('#');
                sensorType = parseInt(split[0]);
                coordinates = split[1];

                var param = configService.getSensor(sensorType);
                if (!coordinates && param.subCharts) {
                    coordinates = _.range(1, param.subCharts.length + 1); // [1, 2, 3]
                }

                function addPromise (coord_) {
                    var coord = coord_ != undefined ? parseInt(coord_) -1 : undefined; // NaN

                    codeCtrl.addPromise(function () {
                        var deferred = $q.defer();
                        var cancel = scope.$on('chartScope:created/hidden', function(event, chartScope) {
                            if (chartScope.isSameSensor({ remote: remote, sensor: sensorType, coordinate: coord })) {
                                $rootScope.$broadcast('recompute-chart-height'); // Recompute height
                                deferred.resolve();
                                cancel();
                            }
                        });

                        slideShowCtrl[action](remote, sensorType, coord);
                        return deferred.promise;
                    });
                }

                if (!coordinates || coordinates.length == 0) {
                    addPromise();
                } else {
                    _.forEach(coordinates, addPromise);
                }

            });
        }
    };
});

angular.module('IOLabApp').directive('playerShowTrace', function ($q) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerHideTraceLink');

            // TraceIdx starts at 1
            var traces = attrs.playerShowTrace.split(' '),
                sensor = attrs.sensor * 1,
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            _.forEach(traces, function(trace) {
                var traceId, value;
                if (trace[0] == '!') {
                    value = false;
                    traceId = trace.slice(1) * 1 - 1;
                }
                else {
                    value = true;
                    traceId = trace * 1 - 1;
                }

                codeCtrl.addPromise(function () {
                    return $q.when(slideShowCtrl.showTrace(remote, sensor, traceId, value));
                });
            });
        }
    };
});

angular.module('IOLabApp').directive('playerHideTrace', function ($q) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerHideTraceLink');

            // TraceIdx starts at 1
            var traces = attrs.playerHideTrace.split(' '),
                sensor = attrs.sensor * 1,
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            _.forEach(traces, function(trace) {
                var traceId, value;
                if (trace[0] == '!') {
                    value = true;
                    traceId = trace.slice(1) * 1 - 1;
                }
                else {
                    value = false;
                    traceId = trace * 1 - 1;
                }

                codeCtrl.addPromise(function () {
                    return $q.when(slideShowCtrl.showTrace(remote, sensor, traceId, value));
                });
            });
        }
    };
});

angular.module('IOLabApp').directive('playerReverseYAxisSensor', function ($q, configService) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerReverseYAxisSensorLink');

            // TraceIdx starts at 1
            var sensors = attrs.playerReverseYAxisSensor.split(' '),
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            function addPromise (remote, sensor, coord, value) {
                var coordinate = coord != undefined ? parseInt(coord) -1 : undefined; // NaN

                codeCtrl.addPromise(function () {
                    return $q.when(slideShowCtrl.reverseYAxis(remote, sensor, coordinate, value));
                });
            }

            _.forEach(sensors, function(sensor) {
                var sensorType, value, split, coordinates;
                if (sensor[0] == '!') {
                    value = false;
                    sensor = sensor.slice(1);
                }
                else {
                    value = true;
                }

                split = sensor.split('#');
                sensorType = parseInt(split[0]);
                coordinates = split[1];

                var param = configService.getSensor(sensorType);
                if (!coordinates && param.subCharts) {
                    coordinates = _.range(1, param.subCharts.length + 1); // [1, 2, 3]
                }

                if (!coordinates || coordinates.length == 0) {
                    addPromise(remote, sensorType, undefined, value);
                } else {
                    _.forEach(coordinates, function(coordinate) {
                        addPromise(remote, sensorType, coordinate, value);
                    });
                }
            });
        }
    };
});

angular.module('IOLabApp').directive('playerSmoothingValue', function ($q, configService) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerSmoothingValue');

            // TraceIdx starts at 1
            var sensors = attrs.sensor.split(' '),
                value = attrs.playerSmoothingValue * 1 || 1,
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            // Value must be odd
            value = value & 1 ? value : value + 1;

            function addPromise (remote, sensor, coord, value) {
                var coordinate = coord != undefined ? parseInt(coord) -1 : undefined; // NaN

                codeCtrl.addPromise(function () {
                    return $q.when(slideShowCtrl.traceSmoothing(remote, sensor, coordinate, value));
                });
            }

            _.forEach(sensors, function(sensor) {
                var sensorType, split, coordinates;

                split = sensor.split('#');
                sensorType = parseInt(split[0]);
                coordinates = split[1];

                var param = configService.getSensor(sensorType);
                if (!coordinates && param.subCharts) {
                    coordinates = _.range(1, param.subCharts.length + 1); // [1, 2, 3]
                }

                if (!coordinates || coordinates.length == 0) {
                    addPromise(remote, sensorType, undefined, value);
                } else {
                    _.forEach(coordinates, function(coordinate) {
                        addPromise(remote, sensorType, coordinate, value);
                    });
                }
            });
        }
    };
});

angular.module('IOLabApp').directive('playerYAxisRange', function ($q) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerYAxisRangeLink');

            var range = _.map(attrs.playerYAxisRange.split(' '), function(val) {
                    return val * 1;
                }),
                sensor = attrs.sensor,
                remote = attrs.remote * 1 || 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];
            
            var split = sensor.split('#'),
                sensorType = parseInt(split[0]),
                coordinates = split[1];
            
            function addPromise (coord_) {
                var coordinate = coord_ != undefined ? parseInt(coord_) -1 : undefined; // NaN

                codeCtrl.addPromise(function () {
                    return $q.when(slideShowCtrl.setYAxisRange(remote, sensorType, coordinate, range));
                });
            }

            if (!coordinates || coordinates.length == 0) {
                addPromise();
            } else {
                _.forEach(coordinates, addPromise);
            }
        }
    };
});

angular.module('IOLabApp').directive('playerSetAcquisitionDuration', function ($q) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {
            console.log('playerSetAcquisitionDurationLink');

            var duration = attrs.playerSetAcquisitionDuration * 1,

                slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            codeCtrl.addPromise(function () {
                return $q.when(slideShowCtrl.setAcquisitionDuration(duration));
            });
        }
    };
});

angular.module('IOLabApp').directive('playerStartAcquisition', function ($q, $timeout, IOLab) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {

            console.log('playerStartAcquisitionLink');

            // remote needed for the check
            var remote = attrs.remote * 1 || 1,
                duration = attrs.duration * 1 || null;

            var slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            // Return promise
            function startAcquisition() {

                var deferred = $q.defer();
                var listener = scope.$watch(
                    function () { return IOLab.hardwareState(); },
                    function () {

                        var iolabRemote1 = IOLab.hardwareState(1);
                        var iolabRemote2 = IOLab.hardwareState(2);

                        if (iolabRemote1.fixedConfig == 0 && iolabRemote2.fixedConfig == 0) {
                            slideShowCtrl.remoteAlert = {
                                message: "Not remote configured !"
                            };
                            return;
                        }

                        if (iolabRemote1.fixedConfig > 0 && (iolabRemote1.rfStatus == 0 || iolabRemote1.configured == false)) {
                            slideShowCtrl.remoteAlert = {
                                message: "The remote 1 is not detected, check if the dongle is plugged and the remote powered on"
                            };
                            return;
                        }

                        if (iolabRemote2.fixedConfig > 0 && (iolabRemote2.rfStatus == 0 || iolabRemote2.configured == false)) {
                            slideShowCtrl.remoteAlert = {
                                message: "The remote 2 is not detected, check if the dongle is plugged and the remote powered on"
                            };
                            return;
                        }

                        slideShowCtrl.remoteAlert = false;

                        listener();

                        var acquisitionSetDuration = IOLab.acquisitionSet().duration;

                        // Wait data acquisition stop to resolve()
                        var cancel = scope.$on('dataStopped', function(event) {
                            cancel();
                            deferred.resolve();
                        });

                        IOLab.startData(duration || acquisitionSetDuration);
                    },
                    true
                );
                return deferred.promise;
            }

            codeCtrl.addPromise(startAcquisition);

        }
    };
});

angular.module('IOLabApp').directive('playerStopAcquisition', function ($q, $timeout, IOLab) {
    "use strict";

    return {
        restrict: 'A',
        require: '^codeBlock',
        link: function (scope, element, attrs, ctrl) {

            console.log('playerStopAcquisitionLink');

            ctrl.addPromise(function () {
                return $q.when(IOLab.stopData());
            });
        }
    };
});

angular.module('IOLabApp').directive('playerNewRun', function ($q, $timeout, IOLab, sensorSelectorService) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {

            console.log('playerNewRunLink');

            var slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];
            
            function newRun () {

                var deferred = $q.defer();
                var listener = scope.$watch(
                    function () { return IOLab.hardwareState().dongleMode; },
                    function (dongleMode) {

                        if (dongleMode !== 8) {
                            slideShowCtrl.remoteAlert = true;
                            return;
                        }

                        slideShowCtrl.remoteAlert = false;
                        listener();

                        IOLab.newAcquisitionSetRun()
                            .then(function() {
                                return IOLab.getAcquisition();
                            })
                            .then(function() {
                                if (IOLab.acquisition(1).config > 0 && IOLab.hardwareState(1).fixedConfig == 0) {
                                    return sensorSelectorService.checkFixedConfig(1);
                                }
                            })
                            .then(function() {
                                if (IOLab.acquisition(2).config > 0 && IOLab.hardwareState(2).fixedConfig == 0) {
                                    return sensorSelectorService.checkFixedConfig(2);
                                }
                            })
                            .then(function() {
                                deferred.resolve();
                            });
                    },
                    true
                );
                return deferred.promise;
            }

            // Return promise
            codeCtrl.addPromise(newRun);
        }
    };
});

angular.module('IOLabApp').directive('playerTimeoutDuration', function ($q, $timeout) {
    "use strict";

    return {
        restrict: 'A',
        require: '^codeBlock',
        link: function (scope, element, attrs, ctrl) {

            console.log('playerWaitLink');

            // playerTimeoutDuration value in second
            var duration = attrs.playerTimeoutDuration.replace(',', '.') * 1000 || 1000;

            // Return promise
            function wait() {
                var deferred = $q.defer();
                $timeout(function () {
                    return deferred.resolve();
                }, duration);

                return { deferred: deferred };
            }

            ctrl.addPromise(wait);
        }
    };
});

angular.module('IOLabApp').directive('playerSaveSlide', function ($q) {
    "use strict";

    return {
        restrict: 'A',
        require: ['^slideShow', '^codeBlock'],
        link: function (scope, element, attrs, ctrl) {

            console.log('playerSaveSlideLink');

            var slideShowCtrl = ctrl[0],
                codeCtrl = ctrl[1];

            codeCtrl.addPromise(function() {
                return $q.when(slideShowCtrl.saveSlide());
            });
        }
    };
});

//
// Button
//

/*
 *  Allowed attrs: save, next, stop-acquisition
 */
angular.module('IOLabApp').directive('btn', function($q, $timeout, IOLab, configService) {
    "use strict";

    return {
        scope: true,
        replace: true,
        restrict: 'E',
        transclude: true,
        require: ['^slideShow', '?^slide'],
        template:
            '<button class="btn btn-sm btn-default btn-player" ng-click="buttonClicked()" ' +
            '         ng-disabled="slideCtrl.slideData.savedOn ||' +
            '                      isAcquisitionSetEmpty() ||' +
            '                      slideShowCtrl.remoteAlert">' +
            '   <div ng-transclude></div>' +
            '   <span>{{label || type.label}}</span>' +
            '</button>',
        link: function(scope, element, attrs, ctrls) {

            var btnTypes = [
                    { label: 'Unknown Button'},
                    { attr: 'save', action: 'save', label: 'Save slide' },
                    { attr: 'next', action: 'next', label: 'Save & Go Next' },
                    { attr: 'stop', action: 'stop', label: 'Stop acquisition' },
                    { attr: 'stopAcquisition', action: 'stop', label: 'Stop acquisition' },
                ],
                slideShowCtrl = ctrls[0],
                slideCtrl = ctrls[1];

            scope.slideCtrl = slideCtrl;

            scope.type = _.find(btnTypes, function(type) {
                return attrs.hasOwnProperty(type.attr);
            }) || btnTypes[0];

            scope.label = attrs.label;

            function stopAcquisition () {
                var dongleMode = configService.getDongleMode(IOLab.hardwareState().dongleMode).desc;

                if (dongleMode != 'DATA') {
                    return $q.when();
                }

                var deferred = $q.defer();
                var cancel = scope.$on('dataStopped', function() {
                    // To let the chart data loading to start...
                    $timeout(deferred.resolve, 50);
                    cancel();
                });
                IOLab.stopData();
                return deferred.promise;
            }

            scope.isAcquisitionSetEmpty = function () {
                return (slideShowCtrl.chartList.length > 0 || slideShowCtrl.showSensorSelector) && IOLab.acquisitionSet().runs.length == 0
            };

            scope.buttonClicked = function() {
                switch(scope.type.action) {

                    case 'stop':
                        $q.when()
                            .then(stopAcquisition);
                        break;

                    case 'save':
                        $q.when()
                            .then(stopAcquisition)
                            .then(slideShowCtrl.saveSlide);
                        break;

                    case 'next':
                        $q.when()
                            .then(stopAcquisition)
                            .then(slideShowCtrl.saveSlide)
                            .then(slideShowCtrl.nextSlide);
                        break;
                }
            };
        }
    }
});

/*  */
angular.module('IOLabApp').directive('renderNestedHtml', function() {
    return {
        compile: function(element) {
            var innerElement = element[0].innerHTML,
                i = 0, nbr;

            while (innerElement[i] == ' ') { i++; }

            nbr = Math.max(2, i - 2);

            var text = element[0].innerHTML
                .replace(/</g, '&&&&<@@@@')
                .replace(/>/g, '&&&&>@@@@')
                .replace(/&&&&/g, '<span>')
                .replace(/@@@@/g, '</span>')
                .replace(/=""/g, '')
                .replace(/\n\ *$/, '')     // Remove last empty line
                .replace(new RegExp('^\ {' + nbr + '}', 'g'), '')     // Remove nbr leading space
                .replace(new RegExp('\n\ {' + nbr + '}', 'g'), '\n'); // Remove nbr leading space

            element.html(text);

        }
    }
});

// Provide fixedConfigs object with configuration
angular.module('IOLabApp').directive('playerAvailableConfigs', function() {
    return {
        controller: function($scope, configService) {
            $scope.fixedConfigs = _(configService.getFixedConfigs())
                .map(function(config, code) {
                    var configuration = angular.copy(config);
                    configuration.code = code * 1;
                    _.map(configuration.sensors, function(sensor) {
                        sensor.desc = configService.getSensor(sensor.sensorKey).desc;
                    });
                    return configuration;
                })
                .sortBy('code')
                .value();
        }
    }
});

angular.module('IOLabApp').directive('mathjax', function() {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            var $script = angular.element("<script type='math/tex'>").html(attrs.mathjax);
            element.html("");
            element.append($script);
            MathJax.Hub.Queue(["Typeset", MathJax.Hub, element[0]]);
        }
    };
});
angular.module('IOLabApp').controller('OptionsCtrl', function ($scope, $rootScope, $modal, chartListService) {
    'use strict';

    $scope.$on('openAboutModal', function() {
        $scope.openAboutModal();
    });

    $scope.openAboutModal = function () {
        $modal.open({
            templateUrl: 'views/modal-about.html',
            backdrop: 'static',
            controller: 'ModalAboutCtrl'
        });
    };

    $scope.setOutputConfig = function (remote) {
        $rootScope.$broadcast('showOutputConfig', remote);
    };

    $scope.chartToDisplayOptions = [
        { val: 0, label: 'Auto' },
        { val: 1, label: '1' },
        { val: 2, label: '2' },
        { val: 3, label: '3' },
        { val: 4, label: '4' },
        { val: 5, label: '5' }
    ];

    $scope.chartListService = chartListService;

    $scope.chartHeightLabel = function() {
        return chartListService.getChartToDisplay() || 'Auto';
    };

});

angular.module('IOLabApp').controller('CalibrationCtrl', function ($scope, $rootScope, $modal, IOLab, configService, sensorSelectorService) {
    'use strict';

    // In the case we open the modal when the sensors are configured,
    // we need to know in the $watch if the configuration change is
    // due to a calibration process start, or due to the regular
    // acquisition process.
    var calibrationRunning = false;

    $scope.openCalibrationModal = function (remote, config) {
        if (remote === 1 && $scope.r1Disabled) return;
        if (remote === 2 && $scope.r2Disabled) return;

        calibrationRunning = true;
        IOLab.setFixedConfig(remote, config);
    };

    var openCalibrationModal = function (config, remote) {
        var modalInstance = $modal.open({
            templateUrl: 'views/modal-calibration.html',
            backdrop: 'static',
            resolve: {
                remote: function () { return remote; },
                config: function () { return config; }
            },
            controller: 'ModalCalibrationCtrl'
        });

        modalInstance.opened
            .then(IOLab.startData);

        modalInstance.result
            .finally(IOLab.stopData);
    };

    $scope.$on('fixedConfigChanged', function (even, args) {
        if (!calibrationRunning) {
            return;
        }

        IOLab.getHardwareState()
            .then(function() {
                openCalibrationModal(args.fixedConfig, args.remote);
            })
    });

    $scope.$on('dataStopped', function () {
        if (!calibrationRunning) {
            return;
        }

        IOLab.resetAcquisition();
        sensorSelectorService.reset();
        calibrationRunning = false;
    });

    $scope.openExpertModeCalibrationModal = function () {
        $modal.open({
            templateUrl: 'views/modal-calibration-expert.html',
            backdrop: 'static',
            controller: 'ModalCalibrationExpertCtrl'
        });
    };

});

angular.module('IOLabApp').controller('OutputConfigCtrl', function ($scope, $attrs, IOLab) {
    'use strict';

    var remote = parseInt($attrs.remote, 10);

    $scope.remote = remote;
    $scope.showOutputConfig = false;
    $scope.expertOptions = true;

    $scope.$on('showOutputConfig', function (event, arg) {
        if (arg === remote) {
            $scope.showOutputConfig = !$scope.showOutputConfig;
        }
    });

    // For generic output
    $scope.output = {};
    $scope.setOutputConfig = function () {
        IOLab.setOutputConfig(
            remote,
            parseInt($scope.output.sensorID, 10),
            parseInt($scope.output.key, 10),
            parseInt($scope.output.value, 10)
        );
    };

    // D6 outPut
    function initD6() {
        $scope.D6Level = IOLab.outputConfigValue(remote, 20, 1) < 1 ? 0 : 1;
        $scope.D6State = IOLab.outputConfigValue(remote, 20, 0) < 2 ? 0 : 2;
        $scope.remoteOff = IOLab.hardwareState(remote).rfStatus === 0;
    }
    initD6();

    $scope.D6LevelChange = function () {
        IOLab.setOutputConfig(remote, 20, 1, parseInt($scope.D6Level, 10));
    };
    $scope.D6StateChange = function () {
        IOLab.setOutputConfig(remote, 20, 0, parseInt($scope.D6State, 10));
    };

    // Watch the remote output config state changes
    $scope.$on('hardwareStateChanged', initD6);
});

angular.module('IOLabApp').controller('ModalAboutCtrl', function($scope, IOLab, $window, IOLabVersion) {
    'use strict';

    $scope.hardwareState = IOLab.hardwareState();
    $scope.$on('hardwareStateChanged', function () {
        $scope.hardwareState = IOLab.hardwareState();
    });

    $scope.iolabVersion = IOLabVersion.value;
    $scope.processVersions = $window.process.versions;
});

angular.module('IOLabApp').controller('ModalCalibrationExpertCtrl', function ($scope, $timeout, configService , IOLab, mgtCMD) {
    'use strict';

    var listOfCalibrationSensors = [1, 2, 3, 8];
    var calGroupedByRemote = [];

    function getOrigCalibration(calibration) {
        var remoteOrig = _.find(calGroupedByRemote, {remoteID: calibration.remoteID});
        return _.find((remoteOrig || {}).calibrations, {sensor: calibration.sensor});
    }

    function removeCalibration(remote, calibration) {
        _.remove(remote.calibrations, function(cal) {
            return cal.sensor === calibration.sensor;
        });
        remote.addSensorList.push(calibration.sensor);
        remote.activeTab[remote.calibrations[0] || 0] = true;
    }

    function setErrorMessage (remote, message) {
        remote.errorMessage = message;
        $timeout.cancel(remote.errorMessageTimeout);
        remote.errorMessageTimeout = $timeout(function() {
            remote.errorMessage = null;
        }, 3 * 1000);
    }
    function setInfoMessage (remote, message) {
        remote.infoMessage = message;
        $timeout.cancel(remote.infoMessageTimeout);
        remote.infoMessageTimeout = $timeout(function() {
            remote.infoMessage = null;
        }, 1.5 * 1000);
    }

    $scope.filter = { value: null };

    $scope.getSensor = function(sensor) {
        return configService.getSensor(sensor);
    };

    IOLab.manageCalibrations(mgtCMD.GetList)
        .then(function(res) {
            var hardwareState = IOLab.hardwareState();

            $scope.calibrationDir = res.calibrationDir;

            // Keep a copy of all calibration
            $scope.calGroupedByRemote = _(res.calibrationList)
                .groupBy('remoteID')
                .map(function(calibrations, remoteID) {
                    var sensors = _.map(calibrations, function(cal) {
                        return cal.sensor;
                    });
                    var activeTab = {};
                    activeTab[calibrations[0] || 0] = true;
                    return {
                        remoteID: parseInt(remoteID, 10),
                        remoteHexaID: '0x' + parseInt(remoteID, 10).toString(16),
                        calibrations: calibrations,
                        activeTab: activeTab,
                        addSensorList: _.xor(listOfCalibrationSensors, sensors),
                        addSensorListDropdownOpen: false,
                        remote1: hardwareState.remote1.remoteID == remoteID,
                        remote2: hardwareState.remote2.remoteID == remoteID,
                    };
                })
                .sortBy('remoteID')
                .value();

            // Display a "copy" to the user
            calGroupedByRemote = angular.copy($scope.calGroupedByRemote);
        });

    $scope.isUpdatedCalibration = function (calibration) {
        var calibrationOrig = getOrigCalibration(calibration);

        // If new calibration...
        if (!calibrationOrig) return true;

        return !( _.isEqual(calibration.calOffset, calibrationOrig.calOffset)
               && _.isEqual(calibration.calScale, calibrationOrig.calScale));
    };

    $scope.addCalibraton = function (remoteID, sensor) {
        var remote = _.find($scope.calGroupedByRemote, { remoteID: remoteID });
        remote.calibrations.push({
            calibrationDate: (new Date()).toISOString(),
            remoteID: remoteID,
            sensor: sensor
        });

        // Close the dropdown
        remote.addSensorListDropdownOpen = false;

        // Remove the sensor from the sensor to add
        _.remove(remote.addSensorList, function(s) {
            return s == sensor;
        });

        // Set new tab as active
        remote.activeTab[sensor] = true;
    };

    $scope.save = function(remote, calibration) {
        IOLab.manageCalibrations(mgtCMD.Save, calibration)
            .then(function(res) {
                if (!res.success) {
                    return setErrorMessage(remote, res.error);
                }
                setInfoMessage(remote, 'Calibration saved...');

                // Update the original object containing the calibration data
                var calibrationOrig = getOrigCalibration(calibration);
                if (calibrationOrig) {
                    angular.copy(calibration, calibrationOrig);
                } else {
                    var remoteOrig = _.find(calGroupedByRemote, {remoteID: calibration.remoteID});
                    calibrationOrig = angular.copy(calibration);
                    remoteOrig.calibrations.push(calibrationOrig);
                }
            });
    };

    $scope.delete = function(remote, calibration) {
        IOLab.manageCalibrations(mgtCMD.Delete, calibration)
            .then(function(res) {
                if (!res.success) {
                    return setErrorMessage(remote, res.error);
                }
                setInfoMessage(remote, 'Calibration deleted...');

                // Update the original object containing the calibration data
                var remoteOrig = _.find(calGroupedByRemote, {remoteID: calibration.remoteID});
                _.remove(remoteOrig.calibrations, function(cal) {
                    return cal.sensor === calibration.sensor;
                });

                removeCalibration(remote, calibration);
            });
    };

    $scope.cancel = function(remote, calibration) {
        var calibrationOrig = getOrigCalibration(calibration);
        if (calibrationOrig) {
            angular.copy(calibrationOrig, calibration);
        } else {
            removeCalibration(remote, calibration);
        }
    };
});

angular.module('IOLabApp').controller('ModalCalibrationCtrl',
    function ($scope, $modalInstance, $timeout, IOLab, configService, BSON, remote, config, mgtCMD) {
        'use strict';
        var calibrationCMD = mgtCMD;

        $scope.iolabState = IOLab.hardwareState();
        $scope.configService = configService;

        // Value injected in the controller
        $scope.remote = remote;
        $scope.config = config;

        switch(config) {

            case 3: /* Orientation */
                // List of sensor to configure
                $scope.sensorTypeList  = _.map([1, 2, 3] , function(sensor) {
                    return {sensor: sensor, desc: configService.getSensor(sensor).desc, incl: true };
                });
                // Gauge type to display (accelerometer)
                $scope.sensor = IOLab.acquisition(remote).sensors[0];
                break;

            case 4: /* MiniMotion */
                // List of sensor to configure
                $scope.sensorTypeList  = [
                    { sensor: 8, desc: configService.getSensor(8).desc, incl: true }
                ];
                // Gauge type to display (force)
                $scope.sensor = IOLab.acquisition(remote).sensors[1];
                break;

            default:
                $scope.sensorTypeList  = [];
        }

        // Calibration steps
        var calibrationStepList = [];

        if ($scope.config === 3) {
            calibrationStepList = [
                { desc: 'Set the device with the x axis up', imagePath: 'images/iolab+x.png' },
                { desc: 'Set the device with the x axis down', imagePath: 'images/iolab-x.png' },
                { desc: 'Set the device with the y axis up', imagePath: 'images/iolab+y.png' },
                { desc: 'Set the device with the y axis down', imagePath: 'images/iolab-y.png' },
                { desc: 'Set the device with the z axis up', imagePath: 'images/iolab+z.png' },
                { desc: 'Set the device with the z axis down', imagePath: 'images/iolab-z.png' }
            ];
        } else if ($scope.config === 4) {
            calibrationStepList = [
                { desc: 'Set the device on its head (y down)', imagePath: 'images/iolab-y.png' },
                { desc: 'Hang the device from its force probe', imagePath: 'images/iolab-force.png' }
            ];
        }

        $scope.calibrationData = {};
        $scope.calibrationDataList = _.map(calibrationStepList, function(step) {
            var data = [];
            _.forEach($scope.sensorTypeList, function(type) {
                data[type.sensor] = [];
            });
            return {
                data: data,
                validated: false,
                desc: step.desc,
                imagePath: step.imagePath
            }
        });

        // Number of steps in calibration
        $scope.currentStep = 1;
        $scope.numOfSteps = Object.getOwnPropertyNames($scope.calibrationDataList).length;
        $scope.currentPercentage = $scope.currentStep / $scope.numOfSteps * 100;

        // Function that returns the next calibration object
        // Set the variable showSaveButton to true if needed
        function getNextCalibrationData () {

            // Supposed that the array if steps is ordered ...
            var newStep = _.find($scope.calibrationDataList, {validated: false});
            $scope.calibrationData = newStep || {};

            // All data steps validated
            if (!newStep) {
                $scope.$on('dataStopped', function() {
                    $scope.showSaveButton = !newStep;
                });
                IOLab.stopData();
            }
        }

        $scope.validateData = function () {
            var timeoutId;

            var callback = function (json) {
                var newData = json;
                var dataMean = [];
                var dataSigma = [];
                var i = 0;

                newData.data.forEach(function (entry) {
                    var mean = d3.mean(entry, function (d) { return d[2]; });
                    var variance = d3.sum(entry, function (d) {
                            return (d[1] - mean) * (d[1] - mean);
                        }) / (entry.length - 1);

                    dataMean.push(mean.toFixed(0));
                    dataSigma.push(Math.sqrt(variance).toFixed(2));

                    $scope.calibrationData.data[newData.sensorType][i++] = parseFloat(mean.toFixed(2));
                });

                // Check if completely fed (if any sensor data empty => validated = false)
                $scope.calibrationData.validated = true;
                $scope.calibrationData.data.forEach(function(sensorData){
                    if (sensorData.length === 0) {
                        $scope.calibrationData.validated = false;
                    }
                });

                // Get the next one if all sensors are validated (callback returned)
                if ($scope.calibrationData.validated) {
                    getNextCalibrationData();
                    $scope.currentStep = $scope.currentStep + 1;
                    $scope.currentPercentage = $scope.currentStep / $scope.numOfSteps * 100;
                }

                $scope.$$phase || $scope.$digest();
            };

            $scope.sensorTypeList.forEach(function (type) {
                timeoutId = $timeout(function () {
                    var params = {
                        remote: $scope.remote,
                        sensor: type.sensor,
                        startTime: -1.8, // 1.8 seconds
                        endTime: -1,     // Last data
                        rawData: true
                    };

                    IOLab.getSensorData(callback, params);

                }, 2000);
            });
        };

        // Save calibration and close modal
        $scope.sendCalibration = function () {
            var i, calData, calDataJson;

            var newDate = new Date();
            $scope.sensorTypeList.forEach(function (type) {
                if (type.sensor === 1 && type.incl) {
                    // Accelerometer calibration ...
                    calData = [];
                    for (i = 0; i < 6; i++) {
                        calData[i] = $scope.calibrationDataList[i].data[type.sensor];
                    }

                    calDataJson = {
                        sensor: type.sensor,
                        remoteID: $scope.iolabState['remote' + $scope.remote].remoteID,
                        calibrationDate: newDate.toISOString(),
                        calOffset: [(calData[0][0] + calData[1][0]) / 2.0,
                            (calData[2][1] + calData[3][1]) / 2.0,
                            (calData[4][2] + calData[5][2]) / 2.0 ],
                        calScale:  [(calData[0][0] - calData[1][0]) / 2.0 / 9.81,
                            (calData[2][1] - calData[3][1]) / 2.0 / 9.81,
                            (calData[4][2] - calData[5][2]) / 2.0 / 9.81 ]
                    };
                    IOLab.manageCalibrations(calibrationCMD.Save, calDataJson);
                }

                if (type.sensor === 2 && type.incl) {
                    // http://www.ngdc.noaa.gov/geomag-web/#igrfwmm
                    // Magnetometer calibration ...
                    // Belgium  N:19.899 µT, E:  0.210 µT, D: 44.301 µT
                    // Chicago  N:19.054 µT, E: -1.249 µT, D: 50.694 µT
                    calData = [];
                    for (i = 0; i < 6; i++) {
                        calData[i] = $scope.calibrationDataList[i].data[type.sensor];
                    }

                    calDataJson = {
                        sensor: type.sensor,
                        remoteID: $scope.iolabState['remote' + $scope.remote].remoteID,
                        calibrationDate: newDate.toISOString(),
                        calOffset: [(calData[0][0] + calData[1][0]) / 2.0,
                            (calData[2][1] + calData[3][1]) / 2.0,
                            (calData[4][2] + calData[5][2]) / 2.0 ],
                        calScale:  [(calData[0][0] - calData[1][0]) / 2.0 / (-50.7),
                            (calData[2][1] - calData[3][1]) / 2.0 / (-50.7),
                            (calData[4][2] - calData[5][2]) / 2.0 / (-50.7) ]
                    };
                    IOLab.manageCalibrations(calibrationCMD.Save, calDataJson);

                }

                if (type.sensor === 3 && type.incl) {
                    // Gyroscope calibration ...
                    calData = [];
                    for (i = 0; i < 6; i++) {
                        calData[i] = $scope.calibrationDataList[i].data[type.sensor];
                    }

                    calDataJson = {
                        sensor: type.sensor,
                        remoteID: $scope.iolabState['remote' + $scope.remote].remoteID,
                        calibrationDate: newDate.toISOString(),
                        calOffset: [(calData[0][0] + calData[1][0]) / 2.0,
                            (calData[2][1] + calData[3][1]) / 2.0,
                            (calData[4][2] + calData[5][2]) / 2.0 ],
                        calScale:  [815, 815, 815]
                    };
                    IOLab.manageCalibrations(calibrationCMD.Save, calDataJson);
                }

                if (type.sensor === 8 && type.incl) {
                    // Force probe calibration ... scale (blue) : 2.08 [N] = 0.212 [kg] * 9.81 [m/s^2]
                    // Force probe calibration ... scale (red)  : 1.98 [N] = 0.202 [kg] * 9.81 [m/s^2]
                    calData = [];
                    for (i = 0; i < 2; i++) {
                        calData[i] = $scope.calibrationDataList[i].data[type.sensor];
                    }

                    calDataJson = {
                        sensor: type.sensor,
                        remoteID: $scope.iolabState['remote' + $scope.remote].remoteID,
                        calibrationDate: newDate.toISOString(),
                        calOffset: (calData[0][0] - 0x7FF),
                        calScale: (calData[0][0] - calData[1][0]) / 1.986
                    };
                    IOLab.manageCalibrations(calibrationCMD.Save, calDataJson);
                }
            });

            // Close modal
            $modalInstance.close();
        };

        // Start the acquisition and get the first calibrationData object
        getNextCalibrationData();
    });

angular.module('IOLabApp').directive('outputConfig', function() {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        scope: true,
        controller: 'OutputConfigCtrl',
        templateUrl: 'views/tpl-output-config.html'
    }
});
angular.module('IOLabApp').directive('controls', function () {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        controller: 'controlsCtrl',
        templateUrl: 'components/controls.tpl.html'
    };
});


angular.module('IOLabApp')
    .controller('controlsCtrl', function ($scope, $rootScope, $timeout, $modal, $q, $window, IOLab,
                                          Player, IOLabMode, IOLabVersion, sensorSelectorService, chartListService,
                                          chartModeService, sharedChartService, configService, slideShowService,
                                          snapshotService) {
        'use strict';

        $scope.IOLab = IOLab;

        /*
         * Repository management
         */

        // Get the frequency depending on the fixed configuration(s)
        $scope.$on('acquisitionStackChanged', function () {
            var config1 = IOLab.acquisition(1).config || 0;
            var config2 = IOLab.acquisition(2).config || 0;

            var frequencies = [].concat(
                (configService.getFixedConfig(config1) || {}).frequencies || [],
                (configService.getFixedConfig(config2) || {}).frequencies || []);

            $scope.frequencies = _.sortBy(_.unique(frequencies));
            $scope.frequency = { value: $scope.frequencies[0] };
        });

        $scope.$on('repository', function (event, args) {

            // Event fired when repository window is closed
            if (args.closed) {
                $scope.onlineRepository = null;
            }

            // Event fired when repository user authentication changed
            if (args.userAuthenticated !== undefined) {
                $scope.userAuthenticated = args.userAuthenticated;
            }
        });

        $scope.onlineRepository = null;
        $scope.openRepository = function () {
            $scope.userAuthenticated = false;
            $scope.onlineRepository = true;
            IOLab.openIOLabRepository();
        };

        $scope.exportToRepository = function () {

            //  Take a snapshot before fetching snapshots and exporting
            function sensorList (remote) {
                return _(sensorSelectorService.sensors[remote])
                    .map(function(s) { return { remote: remote, sensor: s.code }; })
                    .value();
            }

            snapshotService.saveSnapshot({comment: 'Snapshot pushed to the repository'})
                .then(function(){
                    return snapshotService.getSnapshots();
                })
                .then(function(snapshotList){
                    return IOLab.exportToRepository(
                        $scope.frequency.value,
                        [].concat(sensorList(1), sensorList(2)),
                        snapshotList
                    );
                });
        };

        /*
         *  Start, stop and reset controls
         */

        $scope.recordBtnDisabled = function () {
            return $rootScope.dongleMode != 'CONNECTED' || 
                !($rootScope.r1Configured || $rootScope.r2Configured) || 
                $scope.resetOngoing ||
                IOLab.currentRunIsComplete();
        };

        $scope.addRunBtnDisabled = function () {
            return IOLab.isNewAcquisition() ||
                $rootScope.dongleMode != 'CONNECTED' ||
                $scope.addRunOngoing  ||
                (IOLab.acquisitionSet().duration && !IOLab.currentRunIsComplete());
        };
        
        $scope.resetBtnDisabled = function () {
            return $rootScope.dongleMode == 'DATA' || $scope.resetOngoing;
        };
        
        $scope.startData = function () {
            IOLab.startData(IOLab.acquisitionSet().duration);
        };

        $scope.stopData = function () {
            IOLab.stopData();
        };

        $scope.newRun = function () {

            $scope.addRunOngoing = true;

            IOLab.newAcquisitionSetRun()
                .then(function() {
                    return IOLab.getAcquisition();
                })
                .then(function() {
                    if (IOLab.acquisition(1).config > 0 && IOLab.hardwareState(1).fixedConfig == 0) {
                        return sensorSelectorService.checkFixedConfig(1);
                    }
                })
                .then(function() {
                    if (IOLab.acquisition(2).config > 0 && IOLab.hardwareState(2).fixedConfig == 0) {
                        return sensorSelectorService.checkFixedConfig(2);
                    }
                })
                .then(function() {
                    $scope.addRunOngoing = false;
                });
        };

        $scope.resetDataAndConfig = function () {

            checkDisplayMode();

            // Acquisition stack contains no data or contains data from DB
            // In that case, completely rest the acquisition
            if (IOLab.isNewAcquisition() || 
                IOLab.acquisition().viewMode) {

                $q.when()
                    .then(function() {
                        return IOLab.resetAcquisition();
                    })
                    .then(function() {
                        // Reset sensor selector
                        sensorSelectorService.reset();
                    });

            } else {

                $scope.resetOngoing = true;

                // Otherwise, clear the acquisition and prepare
                // for a new acquisition with the same fixed configuration
                $q.when()
                    .then(function() {
                        return IOLab.resetAcquisition();
                    })
                    .then(function() {
                        return IOLab.getAcquisition();
                    })
                    .then(function() {
                        return sensorSelectorService.checkFixedConfig(1);
                    })
                    .then(function() {
                        return sensorSelectorService.checkFixedConfig(2);
                    })
                    .then(function() {
                        $scope.resetOngoing = false;
                    });
            }
        };

        $scope.resourceManagement = function () {
            $modal.open({
                templateUrl: 'components/controls.resources.modal.html',
                backdrop: 'static',
                controller: 'ModalResourcesCtrl'
            });
        };

        /*
         *  Lesson Player
         *
         *  FIXME - When running, we need first to stop the acquisition
         *  FIXME - before quiting, reloading or changing lesson
         */

        $scope.quitLessonPlayer = function() {
            IOLab.clearReportID();
            IOLab.resetAcquisition();

            Player.setLessonTemplate();
            $timeout(function () {
                IOLabMode.setModeAcquisition();
            }, 150);
        };

        $scope.reloadLesson = function() {
            var currentLesson = Player.currentLesson.pathname;
            IOLab.setReportID();
            IOLab.resetAcquisition();

            Player.setLessonTemplate();
            $timeout(function () {
                Player.setLessonTemplate(currentLesson);
            }, 150);
        };


        /*
         *  Chart type/mode management
         */

        $scope.slideShowService = slideShowService;
        $scope.chartListService = chartListService;

        // Drop down zoom modes
        $scope.chartModes = chartModeService.chartModes;
        $scope.chartModeZoom = { value: $scope.chartModes[1] };

        $scope.chart = {
            mode: chartModeService.selectedChartMode
        };
    
        // Stat, zoom, translate
        $scope.modeChanged = function() {
            chartModeService.setChartMode($scope.chart.mode);
            $rootScope.$broadcast('chartModeChanged', $scope.chart.mode);
        };

        $scope.$watch(
            function() { return chartModeService.selectedChartMode.val; },
            function(newVal, oldVal) {
                if (newVal === oldVal || newVal === $scope.chart.mode.val) {
                    return;
                }
                $scope.chart.mode = chartModeService.selectedChartMode;
                $rootScope.$broadcast('chartModeChanged', $scope.chart.mode);
            }
        );

        /*
         *  Single vs Two remotes mode management
         */

        // Initial values
        $scope.remote1Disabled = false;
        $scope.remote2Disabled = false;
        $scope.remote12Disabled = false;

        function checkDisplayMode() {

            var r1Off = IOLab.hardwareState(1).rfStatus === 0;
            var r2Off = IOLab.hardwareState(2).rfStatus === 0;
            var hardwareRemote1Config = IOLab.hardwareState(1).fixedConfig;
            var hardwareRemote2Config = IOLab.hardwareState(2).fixedConfig;

            var acquisitionRemote1Config = IOLab.acquisition(1).config || 0;
            var acquisitionRemote2Config = IOLab.acquisition(2).config || 0;

            var newAcquisition = IOLab.isNewAcquisition();

            if (IOLab.acquisition().viewMode) {

                $scope.remote1Disabled = acquisitionRemote1Config === 0;
                $scope.remote2Disabled = acquisitionRemote2Config === 0;
                $scope.remote12Disabled = $scope.remote1Disabled || $scope.remote2Disabled;

                if (acquisitionRemote1Config !== 0 && acquisitionRemote2Config !== 0) {
                    $scope.displayMode.value = 3;
                } else if (acquisitionRemote1Config !== 0) {
                    $scope.displayMode.value = 1;
                } else {
                    $scope.displayMode.value = 2;
                }

            } else {

                $scope.remote1Disabled = r1Off && (newAcquisition || hardwareRemote1Config === 0);
                $scope.remote2Disabled = r2Off && (newAcquisition || hardwareRemote2Config === 0);
                $scope.remote12Disabled = $scope.remote1Disabled || $scope.remote2Disabled;

                if ($scope.remote2Disabled) {
                    $scope.displayMode.value = 1;
                }
                if ($scope.remote1Disabled && !$scope.remote2Disabled) {
                    $scope.displayMode.value = 2;
                }
            }
        }

        $scope.$on('hardwareStateChanged', function () {
            checkDisplayMode();
        });

        $scope.$on('acquisitionStackChanged', function () {
            checkDisplayMode();
        });

        // When changing remote to display, send a resize event.
        $scope.$watch('displayMode.value', function () {
            // Broadcast 'recompute-chart-height' to resize the charts (wait the DOM update)
            $timeout(function () {$rootScope.$broadcast('recompute-chart-height'); }, 1100);
        });

        checkDisplayMode();
    });

angular.module('IOLabApp')
    .controller('ModalResourcesCtrl', function ($rootScope, $scope, $q, $timeout, $filter, IOLab, IOLabMode, Player, configService,
                                                sensorSelectorService, sharedChartService, mgtCMD) {
        'use strict';

        $scope.currentAcquisitionID = (IOLab.acquisition() || {}).ID;
        $scope.currentReportID = (IOLab.acquisition() || {}).reportID;
        $scope.currentLesson = Player.currentLesson.pathname;

        // Activate the tab depending on the current IOLabMode
        $scope.activeTab = {
            lessonPlayer: IOLabMode.isLessonPlayerMode(),
            acquisition: IOLabMode.isAcquisitionMode()
        };

        $scope.isActivityOpen = {};

        $scope.filterText = {acq: '', les: ''};

        //
        // Acquisition
        //

        // Search filter targets
        $scope.acqFilterTargets = ['Date', 'duration', 'userData.userName', 'userData.comment',
            'remote1.remoteID', 'remote2.remoteID', 'remote1.configDesc', 'remote2.configDesc'];

        //  Acquisition infinite scroll logic
        $scope.acquisitionList = [];
        $scope.acqPageSize = 15;
        $scope.acqPageAddSize = 5;
        $scope.acqLoader = { busy: false };

        var acqSearchQueryListener = $scope.$watch('filterText.acq', function (newQueryString) {
            
            if (newQueryString)
                $scope.filteredAcquisitionList = $filter('customPropsFilter')($scope.completeAcquisitionList, {Query: newQueryString, Fields: $scope.acqFilterTargets});
            else
                $scope.filteredAcquisitionList = $scope.completeAcquisitionList;

            $scope.acquisitionList = _.take($scope.filteredAcquisitionList, $scope.acqPageSize);
        });

        $scope.nextAcquisitionPage = function () {

            if ($scope.acqLoader.busy){
                return;
            }

            $scope.acqLoader.busy = true;
            $scope.acquisitionList = $scope.acquisitionList.concat(_.slice($scope.filteredAcquisitionList, $scope.acquisitionList.length, $scope.acquisitionList.length + $scope.acqPageAddSize));
            $scope.acqLoader.busy = false;
        };

        IOLab.manageAcquisitions(mgtCMD.GetList)
            .then(function (json) {

                //  Simple format manipulation to simplify searches
                _.forEach(json.acquisitionList, function(acq){

                    if(acq.remote1 && acq.remote1.fixedConfig){
                        acq.remote1.configDesc = configDesc(acq, 1);
                    }

                    if(acq.remote2 && acq.remote2.fixedConfig){
                        acq.remote2.configDesc = configDesc(acq, 2);
                    }
                });

                $scope.completeAcquisitionList = _.sortBy(json.acquisitionList, 'lastStartDate').reverse();
                $scope.filteredAcquisitionList = $scope.completeAcquisitionList;
                $scope.acquisitionList = _.take($scope.filteredAcquisitionList, $scope.acqPageSize);
            });

        //  Acquisition management

        $scope.reloadAcquisition = function (acq) {

            var acqID = acq.acquisitionID;

            $scope.$close();
            sensorSelectorService.reset();

            IOLab.clearReportID();
            IOLab.resetAcquisition();
            Player.setLessonTemplate();
            $timeout(function () {
                IOLabMode.setModeAcquisition();
                IOLab.manageAcquisitions(mgtCMD.Reload, acqID)
                    .then(function() {
                        // Called before the acquisition is updated in the client app
                        IOLab.pushAcquisitionSetRun(acqID);
                    });
            }, 150);
        };

        $scope.deleteAcquisition = function (acqID) {
            IOLab.manageAcquisitions(mgtCMD.Delete, acqID)
                .then(function() {
                    return IOLab.manageAcquisitions(mgtCMD.GetList);
                })
                .then(function (json) {
                    $scope.completeAcquisitionList = _(json.acquisitionList).sortBy('lastStartDate').reverse().value();
                    $scope.filteredAcquisitionList = $scope.completeAcquisitionList;
                    $scope.acquisitionList = _.take($scope.filteredAcquisitionList, $scope.acqPageSize);
                });
        };

        // Sensors in the acquisition
        $scope.sensors = function (acq, remote) {
            var acqRemote = acq['remote' + remote];

            if (!acqRemote.sensors) {
                acqRemote.sensors = configService.getFixedConfig(acq['remote' + remote].fixedConfig).sensors;
                _.map(acqRemote.sensors, function (sensor) {
                    sensor.shortDesc = configService.getSensor(sensor.sensorKey).shortDesc;
                });
            }

            return acqRemote.sensors
        };

        // Description of the fixed configuration
        function configDesc (acq, remote) {
            return configService.getFixedConfig(acq['remote' + remote].fixedConfig).desc;
        }
        
        //
        // Lesson
        //

        // Search filter targets
        $scope.lessonFilterTargets = ['name', 'slideShow.author', 'slideShow.description', 'slideShow.id', 'file.name'];

        $scope.filteredLessonList = [];
        $scope.lessonList = [];

        IOLab.manageLessons(mgtCMD.GetList)
            .then(function (json) {
                return $q.all(_.map(json.lessonList, Player.lessonFileParser))
                    .then(function(result) {
                        _.remove(result, function(item) {
                            return !!item.error;
                        });

                        $scope.lessonList = _.sortBy(result, 'lastStartDate').reverse();

                        _.forEach($scope.lessonList, function(lesson) {
                            $scope.isActivityOpen[lesson.slideShow.id] = 
                                lesson.file.pathname === $scope.currentLesson;
                        });

                        //  Set up visible lesson list
                        $scope.filteredLessonList = $scope.lessonList;
                    });
            })
            .then(function() {
                return IOLab.manageReports(mgtCMD.GetList);
            })
            .then(function(json) {
                $scope.reportList = json.reportList;
                distributeReports();
            });

        $scope.loadLesson = function (lesson, report) {
            var pathname = lesson.file.pathname;
            var reportID = (report || {}).reportID;

            $scope.$close();
            sensorSelectorService.reset();

            IOLab.setReportID(reportID);
            IOLab.resetAcquisition();
            Player.setLessonTemplate();
            $timeout(function () {
                Player.setLessonTemplate(pathname, report)
                    .then(function() {
                        IOLabMode.setModeLessonPlayer();
                    });
            }, 150);
        };

        $scope.deleteReport = function (reportID) {
            IOLab.manageReports(mgtCMD.Delete, reportID)
                .then(function() {
                    return IOLab.manageReports(mgtCMD.GetList)
                })
                .then(function (json) {
                    $scope.reportList = json.reportList;

                    distributeReports();
                });
        };

        $scope.exportReport = function (reportID) {
            IOLab.exportReport(reportID);
        };

        $scope.importReport = function () {

            IOLab.importReport()
                .then(function(importedReport) {
                    $scope.importedReport = { report: importedReport };

                    // Try to find the linked lesson/Activity
                    var lesson = _.find($scope.lessonList , function(l) {
                        return l.slideShow.id == importedReport.slideShow.id;
                    });

                    if (!lesson) {
                        $scope.importedReport.message = 'No related activity found. Impossible to import the report.';
                        $scope.importedReport.noActivityFound = true;
                        return;
                    }

                    // Open the activity accordeon
                    $scope.isActivityOpen = {};
                    $scope.isActivityOpen[lesson.slideShow.id] = true;

                    // Check if the report already exists
                    var report = _.find($scope.reportList, function(r) {
                        return r.reportID == importedReport.reportID;
                    });

                    console.log('imported report', report);
                    
                    if (report) {
                        $scope.importedReport.message = 'The report already exists. Confirm to override it.';
                        return;
                    }
                });
        };

        $scope.toggleActivityReports = function(lesson, $event) {
            $event.stopPropagation();
            $scope.isActivityOpen[lesson.slideShow.id] = !$scope.isActivityOpen[lesson.slideShow.id];
        };
        
        $scope.confirmImportReport = function($event) {
            $event.stopPropagation();
            IOLab.confirmImportReport($scope.importedReport.report.reportID)
                .then(function() {
                    // Remove old one if present
                    _.remove($scope.reportList, function(report) {
                        return $scope.importedReport.report.reportID === report.reportID;
                    });
                    $scope.reportList.push($scope.importedReport.report);
                    $scope.importedReport = null;

                    distributeReports();
                });
        };

        $scope.cancelImportReport = function($event) {
            $event.stopPropagation();
            $scope.importedReport = null;
        };

        $scope.updateLesson = function(updatedReport){

            var lessonReportToUpdate = null;

            var lessonToUpdate = _.find($scope.filteredLessonList, function(lesson){

                lessonReportToUpdate = _.find(lesson.reports, function(report){
                    return report.reportID === updatedReport.reportID;
                });

                return !!lessonReportToUpdate;
            });

            if(lessonToUpdate && lessonReportToUpdate){
                angular.copy(updatedReport.userData, lessonReportToUpdate.userData);
            }
        };

        //  Distribute reports to their lesson
        function distributeReports (lessonList, reportList){
            //  If there is no argument we distribute all reports in $scope.reportList among all
            //  visible lessons in $scope.filteredLessonList.
            //  Otherwise, we must specify reports (reportList) to distribute among which lessons (lessonList)
            if(arguments.length === 0){
                lessonList = $scope.filteredLessonList;
                reportList = $scope.reportList;
            } else if(arguments.length !== 2){
                throw Error('Argument error in distributeReports(): this function accepts either 0 or 2 arguments');
            }

            var lessonID = 0;
            _.forEach(lessonList, function(lesson) {
                lessonID = _.get(lesson, 'slideShow.id');

                var filteredList = _.filter(reportList, function(report) {
                    return lessonID && lessonID === _.get(report, 'slideShow.id');
                });
                lesson.reports = _.sortBy(filteredList, 'startTime').reverse();
            });
        }

        var lessonSearchQueryListener = $scope.$watch('filterText.les', function (newQueryString) {

            //  Handle filtered lessons
            $scope.filteredLessonList = $filter('customPropsFilter')($scope.lessonList, {Query: newQueryString, Fields: $scope.lessonFilterTargets});
            distributeReports();

            //  Handle filtered reports
            $scope.filteredLessonAcqList = $filter('customPropsFilter')($scope.reportList, {Query: newQueryString, Fields: $scope.acqFilterTargets});

            //  We collect all lessons related to the $scope.filteredLessonAcqList acquisitions which are not yet visible
            //  and feed them their visible reports in $scope.filteredLessonAcqList
            var requiredLessonIds = _.map($scope.filteredLessonAcqList, 'slideShow.id');
            var visibleLessonIds = _.map($scope.filteredLessonList, 'slideShow.id');
            var requiredLessons = _.filter($scope.lessonList, function(lesson){
                return _.contains(requiredLessonIds, lesson.slideShow.id) && !_.contains(visibleLessonIds, lesson.slideShow.id);
            });

            distributeReports(requiredLessons, $scope.filteredLessonAcqList);

            $scope.filteredLessonList = $scope.filteredLessonList.concat(requiredLessons);
        });

        //  Clean query string listener
        $scope.$on('$destroy', function(){
            acqSearchQueryListener();
            lessonSearchQueryListener();
        });
    });

angular.module('IOLabApp').directive('acquisitionUserDataEditor', function(IOLab, mgtCMD) {
    return {
        scope: true,
        controller: function($scope) {

            function init() {
                $scope.acqEditor.userData = {
                    comment: $scope.acq.userData.comment,
                    userName: $scope.acq.userData.userName
                };
            }

            // Make sur the userData object is not undefined
            $scope.acq.userData = $scope.acq.userData || {};

            //
            // Editor scope
            //
            $scope.acqEditor = {
                open: false
            };

            $scope.acqEditor.save = function() {

                IOLab.manageAcquisitions(mgtCMD.Get, $scope.acq.acquisitionID)
                    .then(function(json) {
                        $scope.acq = angular.copy(json);
                        $scope.acq.userData = $scope.acq.userData || {};
                        _.extend($scope.acq.userData, $scope.acqEditor.userData);
                        IOLab.manageAcquisitions(mgtCMD.Save, $scope.acq.acquisitionID, $scope.acq);
                    })
                    .catch(console.log);

                $scope.acqEditor.open = false;
            };

            $scope.acqEditor.cancel = function() {
                init();
                $scope.acqEditor.open = false;
            };

            init();
        }
    }
});

angular.module('IOLabApp').directive('reportUserDataEditor', function(IOLab, mgtCMD) {
    return {
        scope: true,
        controller: function($scope) {

            function init() {
                $scope.editor.userData = {
                    comment: $scope.report.userData.comment,
                    userName: $scope.report.userData.userName
                };
            }

            // Make sur the userData object is not undefined
            $scope.report.userData = $scope.report.userData || {};

            //
            // Editor scope
            //
            $scope.editor = {
                open: false
            };

            $scope.editor.save = function() {

                IOLab.manageReports(mgtCMD.Get, $scope.report.reportID)
                    .then(function(json) {
                        $scope.report = angular.copy(json);
                        $scope.report.userData = $scope.report.userData || {};
                        _.extend($scope.report.userData, $scope.editor.userData);
                        IOLab.manageReports(mgtCMD.Save, $scope.report.reportID, $scope.report);
                    })
                    .then(function(){
                        if($scope.updateLesson) $scope.updateLesson($scope.report);
                    })
                    .catch(console.log);

                $scope.editor.open = false;
            };

            $scope.editor.cancel = function() {
                init();
                $scope.editor.open = false;
            };

            init();
        }
    }
});

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

    var exports = {
            sensors: []
        },
        updateSensorCallbackStack = [],
        selectedConfig = { 1: 0, 2: 0 },
        sensorConfigs = configService.getSensorConfigs();

    // Returns the list of available configurations keys depending
    // on the selected sensors
    function getAvailableConfigs(remote) {
        var configs = [];

        // Check is some sensors have been checked
        var someSensorsChecked = _.some(exports.sensors[remote], {checked: true});

        // If no sensor checked, return the total list of fixed configurations
        if (!someSensorsChecked) {
            return _.uniq(_.map(sensorConfigs, 'configKey'));
        }

        _.forEach(exports.sensors[remote], function (sensor) {

            if (sensor.checked) {
                // config keys for the checked sensor
                var currentSensorConfigs = _.unique(
                    _.map(_.filter(sensorConfigs, {sensorKey: sensor.code}), 'configKey')
                );

                if (configs.length === 0) {
                    // Initialize the configs array with the configs of
                    // the first sensor
                    configs = currentSensorConfigs;
                } else {
                    // Remove the configurations from the configs array
                    // if the configuration is not compatible with the
                    // current sensor
                    _.remove(configs, function (item) {
                        return currentSensorConfigs.indexOf(item) < 0;
                    });
                }
            }
        });

        //console.log('getAvailableConfig', configs, remote);

        return configs;
    }

    // Select the configuration (with the highest frequency)
    function selectConfig(remote, availableConfigs) {
        var checkedSensors = _.filter(exports.sensors[remote], {checked: true});
        var firstSelectedSensor = checkedSensors.length !== 0 ? checkedSensors[0].code : 0;
        var highestRate = 0;
        var configs = [];
        var newConfig = 0;

        // Fill the configs array with possible configuration keys and sample rates for the firstSelectedSensor
        _.forEach(availableConfigs, function (aCvalue) {
            _.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
        _.forEach(configs, function (value) {
            if (parseInt(value.sampleRate, 10) > parseInt(highestRate, 10)) {
                highestRate = value.sampleRate;
                newConfig = value.configKey;
            }
        });

        // Do not change config if we select an other Analog sensor when in 3V3
        if (newConfig == 8 && selectedConfig[remote] == 12) {
            return;
        }

        selectedConfig[remote] = newConfig;
    }

    // Set the property 'available' of the sensors depending
    // on the availableConfigs
    function setAvailableSensors(remote, availableConfigs) {
        var availableSensors = [];

        if (!availableConfigs) {
            availableConfigs = getAvailableConfigs(remote);
        }

        _.forEach(availableConfigs, function (configKey) {
            // List of sensor for the configKey
            var sensorList = _.map(_.filter(sensorConfigs, {configKey: configKey}), function (sensorConfig) {
                return sensorConfig.sensorKey;
            });
            availableSensors = _.unique(availableSensors.concat(sensorList));
        });

        _.forEach(exports.sensors[remote], function (sensor) {
            sensor.available = availableSensors.indexOf(sensor.code) >= 0;
        });
    }

    // Set the property 'sampleRate' of the sensors depending
    // on the selected configuration
    function setSampleRate(remote) {

        // Sensor properties of the selected configuration
        var configSensors = _.filter(sensorConfigs, {configKey: selectedConfig[remote]});

        // Update the sampleRate property of exports.sensors[remote]
        _.forEach(exports.sensors[remote], function (sensor) {
            var configSensor = _.filter(configSensors, {sensorKey: sensor.code});
            sensor.sampleRate = configSensor.length === 0 ? undefined : configSensor[0].sampleRate;
        });

        //console.log('setSampleRate', exports.sensors[remote]);
    }

    // Initialisation (to be done when the service is instanciated)
    function init(remote) {
        if (arguments.length === 0) {
            init(1);
            init(2);
            return;
        }

        // Initialize the sensors list
        exports.sensors[remote] = configService.getSensors();
        setAvailableSensors(remote);

        var currentConfig = IOLab.acquisition(remote).config || 0;

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

            // Select all the available sensors
            _.forEach(exports.sensors[remote], function (sensor) {
                sensor.checked = sensor.available;
            });

            execUpdateSensorCallbacks(remote);
        }
    }

    function execUpdateSensorCallbacks(remote) {
        _.forEach(updateSensorCallbackStack, function(callback) {
            return callback(remote);
        });
    }

    // To be called each time we want to reset the service to fresh state
    exports.reset = function (remote) {
        if (arguments.length === 0) {
            exports.reset(1);
            exports.reset(2);
            return;
        }
        // Initialize the sensors list and set the available sensors
        exports.sensors[remote] = configService.getSensors();
        setAvailableSensors(remote);
        selectedConfig[remote] = 0;
    };

    // Check if we need to send to the API an fixed config change
    // depending on the current config from of the API and the
    // selectedConfig[remote] variable
    exports.checkFixedConfig = function (remote) {

        var selectedConfigInt = parseInt(selectedConfig[remote], 10);
        var dongleMode = configService.getDongleMode(IOLab.hardwareState().dongleMode).desc;

        if (IOLab.isNewAcquisition()) {
            // Previously it was based on acquisition data (IOLab.acquisition(remote).config)
            var currentConfig = IOLab.hardwareState(remote).fixedConfig || 0;

            // Not yet configured
            if (currentConfig === 0) {

                if (selectedConfigInt !== 0 && dongleMode == 'CONNECTED') {
                    return IOLab.setFixedConfig(remote, selectedConfigInt);
                }

            // Configured
            } else {

                // Configuration does not change
                if (selectedConfigInt == currentConfig) {

                    execUpdateSensorCallbacks(remote);

                // Configuration does change and could be '0'
                } else if (dongleMode == 'CONNECTED') {

                    return IOLab.setFixedConfig(remote, selectedConfigInt);
                }
            }
        }
        return $q.when();
    };

    // Function called each time a sensor is checked
    exports.sensorChecked = function (remote, sensor, coordinate) {
        // Sub-sensor
        if (coordinate != undefined) {
            sensor.subCharts[coordinate].checked = !sensor.subCharts[coordinate].checked;
            execUpdateSensorCallbacks(remote);
            return;
        }

        if (IOLab.isNewAcquisition()) {
            sensor.checked = !sensor.checked;

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

            selectConfig(remote, availableConfigs);
            setSampleRate(remote);
            setAvailableSensors(remote, availableConfigs);

            exports.checkFixedConfig(remote);
        }

        if (!IOLab.isNewAcquisition() && sensor.sampleRate) {
            sensor.checked = !sensor.checked;
            execUpdateSensorCallbacks(remote);
        }
    };

    // Used only for switching config 8 <=> 12 (header)
    exports.setSelectedConfig = function (remote, config) {
        selectedConfig[remote] = config;
    };

    exports.isSensorChecked = function (remote, sensorKey, coordinate) {
        var sensorObj = _.find(exports.sensors[remote], { code: sensorKey });

        if (coordinate != undefined) {
            return sensorObj ? !!sensorObj.subCharts[coordinate].checked : false;
        }
        return sensorObj ? !!sensorObj.checked : false;
    };

    exports.registerUpdateChartListCallback = function (callback) {
        updateSensorCallbackStack.push(callback);
    };

    // Set the sensors which are not in the current config not available
    // when entering in data mode
    $rootScope.$on('hardwareStateChanged', function () {

        if (IOLab.hardwareState().dongleMode === 16) { /* DATA mode */
            setAvailableSensors(1, [selectedConfig[1]]);
            setAvailableSensors(2, [selectedConfig[2]]);
        }
    });

    // Init the sensor selector when loading acquisition ...
    $rootScope.$on('acquisitionStackChanged', function(event, newAcq, oldAcq) {

        var newViewMode = newAcq.viewMode,
            oldViewMode = oldAcq.viewMode;

        if ((newViewMode || (!newViewMode && oldViewMode)) && newAcq.ID !== oldAcq.ID)
        {
            init();
        }
    });

    // Update the sensorConfigs when entering/leaving two remote mode
    $rootScope.$watch(
        function () { return IOLab.remotesState(); },
        function (remoteState) {

            // Reset the global sensorConfigs variable
            sensorConfigs = configService.getSensorConfigs();

            // Function to check if the selected config (if any) is still
            // compatible with the low speed configs (two remotes mode)
            function checkConfig(remote) {
                var availableConfigs = getAvailableConfigs(remote);
                if (IOLab.isNewAcquisition() &&
                    selectedConfig[remote] !== 0 &&
                    availableConfigs.indexOf(selectedConfig[remote]) === -1) {

                    // Try to select a config based on the available configs
                    selectConfig(remote, availableConfigs);

                    // If no fixed config corresponds to the selected sensors
                    // reset the checked sensors
                    if (selectedConfig[remote] === 0) {
                        // Reset the checked attributes of all sensors
                        _.forEach(exports.sensors[remote], function (sensor) {
                            sensor.checked = false;
                        });
                        // Feed the availableConfig array with all the low speed configs
                        availableConfigs = getAvailableConfigs(remote);
                    }
                    setSampleRate(remote);
                    setAvailableSensors(remote, availableConfigs);
                    exports.checkFixedConfig(remote);
                }
            }

            // When the two remote are on, check
            if (!remoteState.r1Off && !remoteState.r2Off) {

                // Filter the sensorConfigs to remove highSpeed configs
                sensorConfigs = _.filter(sensorConfigs, { highSpeed: false });

                checkConfig(1);
                checkConfig(2);
            }
        },
        true
    );

    // Initialisation of the service
    init();

    return exports;
});

angular.module('IOLabApp').directive('sensorSelector', function($rootScope, $timeout) {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        scope: true,
        require: ['?^slideShow'],
        controller: 'sensorSelectorCtrl',
        templateUrl: 'components/sensor.selector.tpl.html',
        link: function(scope, element, attrs, ctrls) {
            scope.inPlayer = !!ctrls[0];

            if (scope.inPlayer) {
                scope.$watch('isCollapsed', function() {
                    $timeout(() => {
                        $rootScope.$broadcast('recompute-chart-height');
                    }, 250);
                })
            }
        }
    };
});

angular.module('IOLabApp').controller('sensorSelectorCtrl', function ($scope, $attrs, IOLab,
            sensorSelectorService, snapshotService) {
    'use strict';

    var selector = sensorSelectorService,
        remote = parseInt($attrs.remote, 10);

    $scope.remote = remote;
    $scope.IOLab = IOLab;
    $scope.snapshotService = snapshotService;

    $scope.fixedConfigService = sensorSelectorService;

    $scope.currentConfig = function () {
        return IOLab.acquisition(remote).config;
    };

    $scope.rfFwVersion = function () {
        return IOLab.hardwareState(remote).rfFwVersion;
    };

    $scope.selectConfig = function (remote, config) {
        IOLab.setFixedConfig(remote, config)
            .then(function () {
                sensorSelectorService.setSelectedConfig(remote, config);
            });
    };

    $scope.$on('hardwareStateChanged', function (event, newState, oldState) {

        // When the dongle enter in CONNECTED mode, try to set the fixed config
        // if already set in the sensor selector
        //  => Check if the new dongle state is CONNECTED
        //
        //  OR
        //
        // When two remotes are paired, we need to check when a remote is powered on
        // if we need to set the fixed config for that remote. Note that we need also to
        // check the dongle mode since the rfStatus is sent before the dongle mode is set
        // to CONNECTED.
        //  => Check if the new dongle state is CONNECTED & if the remote is now powered on

        if (newState.dongleMode === 8 && (oldState.dongleMode !== 8 ||
                (newState['remote' + remote].rfStatus !== 0 && oldState['remote' + remote].rfStatus === 0))) {
            selector.checkFixedConfig(remote);
        }
    });
});

angular.module('IOLabApp').directive('iolabChart', function ($q, $rootScope, $timeout, IOLab, chartService,
                                                             windowResizeService, helperService, configService, 
                                                             snapshotService, sharedChartService, chartModeService, chartListService) {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        scope: true,
        require: ['iolabChart', '?^slideShow'],
        controller: 'chartCtrl',
        templateUrl: 'components/chart.tpl.html',
        link: function (scope, element, attrs, ctrls) {

            var remote = scope.sensor.remote,
                sensorType = scope.sensor.sensorType,
                coordinate = scope.sensor.coordinate,

                chartCtrl = ctrls[0],
                slideShowCtrl = ctrls[1];

            // D3 class object
            var chart = chartService(scope.sensor);
            chartCtrl.chart = chart;

            // html chart element
            var chartElement = d3.select(element[0]);

            // Set height & width
            chart.height((slideShowCtrl || chartListService).getSvgHeight(remote));
            chart.width(element.parent()[0].clientWidth - (slideShowCtrl ? 20 : 0));

            // Draw the chart element
            chart(chartElement); // idem chartElement.call(chart);
            
            var timeoutId,
                overlap,

                startTime,
                previousLastPoint,
                samplingRate,
                computeSamplingRate,
                pathNbr;

            // Function to execute when finish loading
            var restoreSnapshotFn = null;

            // Over lap only for the wheel...
            if (scope.sensor.sensorType === 9) {
                overlap = 8; /*pt*/
            } else {
                overlap = 0;
            }

            // Used to compute loading time
            var t1;

            function initGlobal () {
                startTime = 0;
                previousLastPoint = 0;
                samplingRate = 1;
                computeSamplingRate = true;
                pathNbr = 1;
            }

            function updateChart() {
                if (scope.running) {
                    timeoutId = $timeout(function () {
                        IOLab.getSensorData(
                            addDataToChart,
                            { remote: remote, sensor: sensorType, startTime: startTime, coordinate: coordinate }
                        );
                    }, 20, false);
                } else {
                    console.log('*** Force cancel timeout !!! ***');
                    $timeout.cancel(timeoutId);
                }
            }

            function addDataToChart(json) {
                var newData = json;

                // if new data empty return
                if (!newData.datalength) {
                    updateChart();
                    return;
                }

                // Check the resampling the first time
                if (computeSamplingRate) {
                    computeSamplingRate = false;
                    samplingRate = chart.resampling(newData.sampleRate);
                }

                // First and last data point (time)
                var dataLength = newData.data[0].length;
                var firstPoint= newData.data[0][0][0];
                var lastPoint = newData.data[0][dataLength - 1][0];

                // Shift the time axis if we are at the end
                if (lastPoint >= chart.scale().x1) {
                    samplingRate = chart.resampling(newData.sampleRate);
                    startTime = chart.scale().x1;
                    chart.removeData('.running');
                    chart.addData(newData.data, { cl: 'path' + (++pathNbr), running: true });
                    chart.shiftTimeScale(lastPoint);

                    // Clear cursor range if set in running mode
                    var range = sharedChartService.cursorRange;
                    if (range && (range.t0 > -1 || range.t1 > -1)) {
                        chart.redrawStatistics({ t0: -1, t1: -1 });
                    }
                    chart(chartElement);
                    updateChart();
                    return;
                }

                // Redraw the preceding path if the new one overlaps
                if (firstPoint < previousLastPoint) {
                    chart.truncateData('path' + pathNbr, overlap);
                }

                // Draw a new path
                chart.addData(newData.data, { cl: 'path' + (++pathNbr), running: true, overlap: overlap });

                // Resampling correction
                var samplingOverlap = Math.max(dataLength - 1 - overlap, 0) % (2 * samplingRate) + 1;
                // Keep the start time for the next data query
                startTime =  newData.data[0][Math.max(dataLength - 1 - overlap - samplingOverlap, 0)][0];
                // Keep the last drawn point for the next step
                previousLastPoint = newData.data[0][Math.max(dataLength - 1 - samplingOverlap, 0)][0];

                updateChart();
            }

            function _loadDataToChart(json, opts) {
                var newData = json;
                var acqID = (opts || {}).acqID || IOLab.acquisition().ID;

                if (newData.datalength === 0) {
                    scope.alert.loading = false;
                    scope.alert.error = true;
                    scope.$$phase || scope.$apply();
                    return;
                }

                chart.removeData('.running');
                chart.bbox(newData.boundingBox, acqID);
                chart.addData(newData.data, { 
                    cl: 'acq-' + acqID,
                    computeScale: opts.computeScale 
                });
                chart.resampling(newData.sampleRate);
                chart.smoothingPath(scope.control.timeAverage);

                chart(chartElement);
                scope.alert.loading = false;
                scope.$$phase || scope.$apply();
            }

            function loadDataToChart(json, acqID) {
                var t2 = new Date();
                console.log('*** stopDataCallBack - getSensorData:', t2 - t1, 'ms');
                _loadDataToChart(json, { computeScale: true, acqID: acqID });
                var t3 = new Date();
                console.log('*** stopDataCallBack - loadDataToChart', t3 - t2, 'ms');
            }

            function reloadDataToChart(json) {
                var t2 = new Date();
                console.log('*** reloadData - getSensorData:', t2 - t1, 'ms');
                _loadDataToChart(json, {});
                var t3 = new Date();
                console.log('*** reloadData - loadDataToChart', t3 - t2, 'ms');
            }

            initGlobal();

            // Load the data if any when the chart is created !
            if (IOLab.acquisitionSet().runs.length > 0) {
                if (scope.running) {
                    computeSamplingRate = true;
                    chart.smoothingPath(1);
                    chart.initScale(startTime);
                    chart(chartElement);
                    updateChart();
                } else {
                    t1 = new Date();
                    scope.alert.loading = true;
                    console.log('*** loadDataToChart - start');

                    // Get all acquisitionSet data

                    var queue = $q.when();

                    _.forEach(IOLab.acquisitionSet().runs, function(run) {
                        queue = queue.then(function() {
                            var deferred = $q.defer();

                            function callback (json) {
                                loadDataToChart(json, run.acqID);
                                deferred.resolve();
                            }
                            IOLab.getSensorData(callback, { acqID: run.acqID, remote: remote, sensor: sensorType, coordinate: coordinate });

                            return deferred.promise;
                        })
                    });

                    // Restore snapshot
                    queue.then(function() {
                        if (restoreSnapshotFn) {
                            restoreSnapshotFn();
                            restoreSnapshotFn = null;

                            chart(chartElement);
                            scope.alert.loading = false;
                            scope.$$phase || scope.$apply();
                        }
                    })
                }
            }

            scope.$on('dataStarted', function () {
                console.log('*** startData ***');
                scope.alert.error = false;
                scope.alert.loading = false;
                scope.running = true;

                computeSamplingRate = true;
                chart.smoothingPath(1);
                chart.initScale(startTime);
                chart(chartElement);
                updateChart();
            });

            scope.$on('dataStopped', function () {
                console.log('*** stopData ***');
                scope.alert.loading = true;
                scope.running = false;

                t1 = new Date();
                // Cancel the timeout
                $timeout.cancel(timeoutId);
                // Get all the data after calibration
                IOLab.getSensorData(loadDataToChart, { remote: remote, sensor: sensorType, coordinate: coordinate});
            });

            scope.$on('sensorRecalibrated', function (event, args) {
                if (args.remote == remote && args.sensorType == sensorType) {
                    console.log('*** sensorRecalibrated ***');
                    scope.alert.loading = true;
                    t1 = new Date();
                    IOLab.getSensorData(reloadDataToChart, { remote: remote, sensor: sensorType, coordinate: coordinate });
                }
            });

            scope.$on('$destroy', function () {
                console.log('*** scope.$on => destroy Chart ***');
                chart.removeData();
                chart.on('cursorPosition', null);
                chart.on('cursorRange', null);
                chart.on('analytics', null);
                chart.on('fftResult', null);
                chart.on('scaleChanges', null);
                chart.on('setMode', null);

                snapshotService.unregisterChart(scope);
                
                if (slideShowCtrl) {
                    slideShowCtrl.unregisterChart(scope);
                }
            });

            element.bind('$destroy', function () {
                $timeout.cancel(timeoutId);
            });


            /*
             *  Resize event handler
             */
            scope.$on('recompute-chart-height', function () {
                chart.height((slideShowCtrl || chartListService).getSvgHeight(remote));
                chart.width(element.parent()[0].clientWidth - (slideShowCtrl ? 20 : 0));
                chart();
            });


            scope.$on('acquisitionSet.newRun', function() {
                initGlobal();
                chart.newRun();
            });

            scope.$on('acquisitionSet.removeRun', function(event, args) {
                chart.removeData('.acq-' + args.acqID);
                chart(chartElement);
            });

            /*
             *  Scale changed from the chart
             */
            chart.on('scaleChanges', function (scale) {
                sharedChartService.scale = scale;
                sharedChartService.sensor = scope.sensor;
                scope.$$phase || scope.$apply();
            });
            scope.$watch(
                function() { return sharedChartService.scale; },
                function() {
                    if (!sharedChartService.scale) return;
                    if (sharedChartService.sensor === scope.sensor) return;
                    chart.scale(sharedChartService.scale);
                    chart();
                }, true
            );

            /*
             *  Cursor position (update of the legend)
             */
            chart.on('cursorPosition', function (d) {
                sharedChartService.cursorPosition = d;
                scope.$$phase || scope.$apply();
            });
            scope.$watch(
                function () { return sharedChartService.cursorPosition; },
                function (newCursorPosition) {
                    if (newCursorPosition) {
                        chart.redrawLegend(newCursorPosition);
                    }
                }
            );

            /*
             *  Cursor range (update statistics)
             */
            chart.on('cursorRange', function (d) {
                sharedChartService.cursorRange = d;
                scope.$$phase || scope.$apply();
            });
            scope.$watch(
                function () { return sharedChartService.cursorRange; },
                function (newCursorRange) {
                    if (newCursorRange) {
                        chart.redrawStatistics(newCursorRange);
                    }
                },
                true
            );

            /*
             *  Redraw the fft chart when fftResult is emitted
             */
            chart.on('fftResult', function (data) {
                if (chartCtrl.fftRedraw) {
                    chartCtrl.fftRedraw(data);
                }
            });

            /*
             *  Redraw stat data
             */
            chart.on('analytics', function (data) {
                if (chartCtrl.setAnalyticsData)
                    chartCtrl.setAnalyticsData(data);
            });

            /*
             *  Mode changed from the chart
             */
            chart.on('setMode', function (newMode) {
                var mode = _.find(chartModeService.chartModes, { val: newMode });
                if (mode) {
                    chartModeService.setChartMode(mode);
                }
            });

            // Init chart mode
            chart.mode(chartModeService.selectedChartMode.val);

            // Chart mode change
            scope.$on('chartModeChanged', function(event, mode) {
                chart.mode(mode.val);
            });

            /*
             *  Chart Controls
             */
            scope.$watch('control.timeAverage', function () {
                if (scope.running) { return; }
                chart.smoothingPath(scope.control.timeAverage);
            });
            scope.$watch('control.fftSampleRate', function () {
                chart.fftSampleRate(scope.control.fftSampleRate);
            });
            scope.$watch('control.reverseAxis', function (value) {
                var factor = value ? -1 : 1,
                    parityFactors = [1];

                switch(sensorType) {
                    case 1:
                    case 3:
                        parityFactors = [1, factor, 1];
                        break;
                    case 8:
                    case 9:
                        parityFactors = [factor];
                        break;
                }
                chart.parityFactors(parityFactors);
            });
            scope.$watch('control.traceSettings', function () {
                chartElement.call(chart.showTraces(scope.control.traceSettings));
            }, true);


            //
            // Snapshot
            //
            snapshotService.registerChart(scope);

            scope.restoreSnapshot = function(scale_, defScale_, cursorRange_) {

                function restoreSnapshot () {
                    if (cursorRange_) {
                        chart.redrawStatistics(cursorRange_);
                        sharedChartService.cursorRange = cursorRange_;
                    }
                    if (defScale_) {
                        chart.scale(defScale_, {setDefScale: true});
                    }
                    if (scale_) {
                        chart.scale(scale_);
                    }
                    chart();
                }

                if (scope.alert.loading) {
                    restoreSnapshotFn = restoreSnapshot;
                } else {
                    restoreSnapshot();
                }
            };

            scope.takeSnapshot = function() {

                var snapshot = {
                    remote: remote,
                    sensor: sensorType,
                    control: {
                        reverseAxis: scope.control.reverseAxis,
                        timeAverage: scope.control.timeAverage,
                        tracesShown: _.map(scope.control.traceSettings, 'show')
                    },
                    scale: chart.scale(),
                    defScale: chart.scale(null, {getDefScale: true}),
                    cursorRange: sharedChartService.cursorRange
                };

                // Saving undefined produce a 'null' in the json :-/
                if (coordinate != undefined) {
                    snapshot.coordinate = coordinate;
                }

                return snapshot;
            };

            //
            // Slide show management
            //
            if (slideShowCtrl) {
                scope.showRemote = true;
                scope.showChart = true;
                slideShowCtrl.registerChart(scope);
            }

            //
            // Init chart when
            //
            //scope.restoreSnapshot(sharedChartService.scale, sharedChartService.cursorRange);

            // Recompute other chart height when rendering the current hart
            $rootScope.$broadcast('recompute-chart-height');
        }
    };
});

angular.module('IOLabApp').controller('chartCtrl', function ($scope, IOLab, configService, helperService, sharedChartService) {
    'use strict';

    var remote, config, sensorType, coordinate, sensorParam, self = this;

    remote = $scope.sensor.remote;
    config = $scope.sensor.config;
    sensorType = $scope.sensor.sensorType;
    coordinate = $scope.sensor.coordinate;
    sensorParam = configService.getSensor(sensorType);

    $scope.chartCtrl = self;
    $scope.showChart = true;

    $scope.open = { tools: false, axisSettings: false };
    $scope.alert = {};

    $scope.isSameSensor = function (snSensor) {
        return remote === snSensor.remote &&
            sensorType === snSensor.sensor &&
            coordinate === snSensor.coordinate
    };
    
    $scope.exportData = function (sensor) {
        var fileName = helperService.getTime() + "_" + sensor.shortDesc;
        IOLab.exportData(sensor.remote, sensor.sensorType, fileName)
            .then(function(res) {
                $scope.$broadcast('alert', { message: res.path, type: 'success' });
            })
            .catch(function(res) {
                $scope.$broadcast('alert', { message: res.error, type: 'danger' });
            });
    };

    $scope.resetSensorOffset = function (sensor) {
        if ($scope.rezeroSensorDisabled()) {
            return;
        }

        var cursorRange = sharedChartService.cursorRange || { t0: -1, t1: -1 };
        if ($scope.running) {
            // Rezeroing using average of 1 second of data from the end
            IOLab.resetSensorOffset(sensor.remote, sensor.sensorType, -0.25, -1)
                .then(function() {
                    return IOLab.recalibrateSensor(sensor.remote, sensor.sensorType);
                });
        } else if (cursorRange.t0 != -1 && cursorRange.t1 && cursorRange.t1 != -1) {
            // Rezeroing using average using selected range
            IOLab.resetSensorOffset(sensor.remote, sensor.sensorType, cursorRange.t0, cursorRange.t1)
                .then(function() {
                    return IOLab.recalibrateSensor(sensor.remote, sensor.sensorType);
                });
        } else {
            IOLab.resetSensorOffset2(sensor.remote, sensor.sensorType)
                .then(function() {
                    return IOLab.recalibrateSensor(sensor.remote, sensor.sensorType);
                });
        }
    };

    // FIXME - Allow for wheel - position (only resetSensorOffset() and not resetSensorOffset2() ) !!!
    $scope.allowResetSensorOffset = function(){
        return $scope.sensor.sensorType == 8 /* Force */ ||
            ($scope.sensor.sensorType == 9 && $scope.sensor.coordinate == 0) /* Wheel - position */;
    };

    $scope.sensorOffsetSet = function(){
        return (_.find(IOLab.acquisition(remote).sensors, { sensorType: sensorType }) || {}).rezeroOffset != null;
    };

    $scope.rezeroSensorDisabled = function() {
        if ($scope.running) {
            return false;
        }

        // Force probe
        if ($scope.sensor.sensorType == 8) {
            if (!IOLab.acquisition().viewMode) {
                return false;
            }
            return !sharedChartService.cursorRange
                || sharedChartService.cursorRange.t0 === -1
                || sharedChartService.cursorRange.t1 === -1;
        }
        return true;
    };

    // Drop down time smoothing
    $scope.timeAverages = [1, 3, 5, 7, 9, 11, 13, 15, 25];

    // Drop down FFT
    $scope.fftSampleRates = [
        { value: null, label: '-' },
        { value: 256,  label: '256' },
        { value: 512,  label: '512' },
        { value: 1024, label: '1024' },
        { value: 2048, label: '2048' },
        { value: 4096, label: '4096' }
    ];

    $scope.running = IOLab.hardwareState().dongleMode === 16;

    // Scope injected in the chart directive and available in the chart view
    $scope.control = {
        fftSampleRate: $scope.fftSampleRates[0].value,
        exportDataDisabled: IOLab.isNewAcquisition(),
        frequency: configService.getSampleRate(sensorType, config),

        reverseAxis: sensorParam.reverseAxis,
        traceSettings: configService.getTraceSettings(sensorType, coordinate),
        timeAverage: $scope.timeAverages[sensorParam.timeAverageIdx || 0]
    };

    $scope.$on('dataStopped', function () {
        $scope.control.exportDataDisabled = IOLab.isNewAcquisition();
    });


    //
    // Y-axis settings
    // Some sensors have specific param (subCharts)
    //

    var param = sensorParam.subCharts && coordinate != undefined
        ? sensorParam.subCharts[coordinate]
        : sensorParam;

    $scope.yAxisSettings = {
        autoScaleY: param.autoScaleY,
        scales: angular.copy(param.scales)
    };

    $scope.yAxisSettingsChanged = function() {
        param.autoScaleY = $scope.yAxisSettings.autoScaleY;
        
        var y0 = parseInt($scope.yAxisSettings.scales[0]),
            y1 = parseInt($scope.yAxisSettings.scales[1]);

        if (!isNaN(y0) && !isNaN(y1)) {
            param.scales[0] = y0;
            param.scales[1] = y1;

            self.chart.scale({y0: y0, y1: y1}, {setDefScale: true});
            self.chart();
        }
    };
    
});
angular.module('IOLabApp').directive('iolabEcgChart', function () {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        controller: 'chartCtrl',
        templateUrl: 'components/chart.ecg.tpl.html'
    };
});

angular.module('IOLabApp').directive('iolabEcgSvg', function ($rootScope, $timeout, IOLab, chartService, windowResizeService,
                                                              helperService, configService, sharedChartService, chartListService, chartModeService) {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        controller: function () { },
        templateUrl: 'components/chart.ecg.svg.html',
        link: function (scope, element, attrs, chartCtrl) {

            // D3 class object (scope.coord comes from ng-repeat)
            // Extend the scope.sensor obj !
            scope.sensor.coordinate = scope.coord;
            var chart = chartService(scope.sensor);

            // html chart element
            var chartElement = d3.select(element[0]);

            // Set width
            chart.width(element.parent()[0].clientWidth);

            // Draw the chart element
            chart(chartElement); // idem chartElement.call(chart);

            var remote = scope.sensor.remote,
                sensorType = scope.sensor.sensorType;

            var timeoutId,
                newData = [],
                overlap = 0,
                startTime = 0,
                samplingRate = 1,
                computeSamplingRate = true,
                pathNbr = 1;

            // Used to compute loading time
            var t1, t2;

            function updateChart() {
                if (scope.running) {
                    timeoutId = $timeout(function () {
                        IOLab.getSensorData(
                            addDataToChart,
                            { remote: remote, sensor: sensorType, startTime: startTime, coordinate: scope.coord }
                        );
                    }, 25, false);
                } else {
                    console.log('*** Force cancel timeout !!! ***');
                    $timeout.cancel(timeoutId);
                }
            }

            function addDataToChart(json) {
                newData = json;

                // if new data empty return
                if (!newData.datalength) {
                    updateChart();
                    return;
                }

                // Check the resampling the first time
                if (computeSamplingRate) {
                    computeSamplingRate = false;
                    samplingRate = chart.resampling(newData.sampleRate);
                }

                // First and last data point (time)
                var dataLength = newData.data[0].length;
                var firstPoint = newData.data[0][0][0];
                var lastPoint = newData.data[0][dataLength - 1][0];

                // Shift the time axis if we are at the end
                if (lastPoint >= chart.scale().x1) {
                    samplingRate = chart.resampling(newData.sampleRate);
                    startTime = chart.scale().x1;
                    chart.removeData('.running');
                    chart.addData(newData.data, { cl: 'path' + (++pathNbr), running: true });
                    chart.shiftTimeScale(lastPoint);
                    chart(chartElement);
                    updateChart();
                    return;
                }

                // Draw a new path
                chart.addData(newData.data, { cl: 'path' + (++pathNbr), running: true, overlap: overlap });

                var samplingOverlap = Math.max(dataLength - 1, 0) % (2 * samplingRate) + 1;
                // Keep the start time for the next data query
                startTime =  newData.data[0][Math.max(dataLength - 1 - (samplingOverlap), 0)][0];

                updateChart();
            }

            function loadDataToChart(json) {
                var acqID = IOLab.acquisition().ID;

                newData = json;
                t2 = new Date();
                console.log('*** stopDataCallBack - getSensorData:', t2 - t1, 'ms');

                if (newData.datalength === 0) {
                    scope.alert.loading = false;
                    scope.alert.error = true;
                    scope.$$phase || scope.$apply();
                    return;
                }

                chart.removeData('.running');
                chart.bbox(newData.boundingBox, acqID);
                chart.addData(newData.data, {
                    cl: 'acq-' + acqID,
                    computeScale: true
                });
                chart.resampling(newData.sampleRate);
                chart.smoothingPath(scope.control.timeAverage);
                chart(chartElement);
                scope.alert.loading = false;
                scope.$$phase || scope.$apply();
            }

            // Load the data if any when the chart is created !
            if (!IOLab.isNewAcquisition()) {
                if (scope.running) {
                    computeSamplingRate = true;
                    chart.smoothingPath(1);
                    chart.initScale(startTime);
                    chart(chartElement);
                    updateChart();
                } else {
                    t1 = new Date();
                    scope.alert.loading = true;
                    IOLab.getSensorData(loadDataToChart, { remote: remote, sensor: sensorType, coordinate: scope.coord });
                }
            }

            scope.$on('dataStarted', function () {
                console.log('*** startData ***');
                scope.alert.error = false;
                scope.alert.loading = false;
                scope.running = true;

                computeSamplingRate = true;
                chart.smoothingPath(1);
                chart.initScale(startTime);
                chart(chartElement);
                updateChart();
            });

            scope.$on('dataStopped', function () {
                console.log('*** stopData ***');
                scope.alert.loading = true;
                scope.running = false;

                t1 = new Date();
                // Cancel the timeout
                $timeout.cancel(timeoutId);
                // Get all the data after calibration
                IOLab.getSensorData(loadDataToChart, { remote: remote, sensor: sensorType, coordinate: scope.coord });
            });

            scope.$on('$destroy', function () {
                console.log('*** scope.$on => destroy Chart ***');
                chart.removeData();
                try{
                    chart.on('cursorPosition', null);
                    chart.on('cursorRange', null);
                    chart.on('fftResult', null);
                    chart.on('fftInit', null);
                    chart.on('scaleChanges', null);
                    chart.on('setMode', null);
                } catch(err) {}
            });

            element.bind('$destroy', function () {
                $timeout.cancel(timeoutId);
            });

            /*
             *  Resize event handler
             */
            scope.$on('recompute-chart-height', function () {
                chart.width(element.parent()[0].clientWidth);
                // Redraw the chart
                chart();
            });

            /*
             *  Scale changed from the chart
             */
            chart.on('scaleChanges', function (scale) {
                sharedChartService.scale = scale;
                sharedChartService.coord = scope.coord;
                scope.$$phase || scope.$apply();
            });
            scope.$watch(
                function() { return sharedChartService.scale; },
                function() {
                    if (!sharedChartService.scale) return;
                    if (sharedChartService.coord === scope.coord) return;
                    chart.scale(sharedChartService.scale);
                    chart();
                }, true
            );

            /*
             *  Cursor position (update of the legend)
             */
            chart.on('cursorPosition', function (d) {
                sharedChartService.cursorPosition = d;
                scope.$$phase || scope.$apply();
            });
            scope.$watch(
                function () { return sharedChartService.cursorPosition; },
                function (newCursorPosition) {
                    if (newCursorPosition) {
                        chart.redrawLegend(newCursorPosition);
                    }
                }
            );

            /*
             *  Cursor range (update statistics)
             */
            chart.on('cursorRange', function (d) {
                sharedChartService.cursorRange = d;
                scope.$$phase || scope.$apply();
            });
            scope.$watch(
                function () { return sharedChartService.cursorRange; },
                function (newCursorRange) {
                    if (newCursorRange) {
                        chart.redrawStatistics(newCursorRange);
                    }
                },
                true
            );

            /*
             *  Redraw stat data
             */
            chart.on('analytics', function (data) {
                if (chartCtrl.setAnalyticsData) {
                    chartCtrl.setAnalyticsData(data);
                }
            });

            /*
             *  Mode changed from the chart
             */
            chart.on('setMode', function (newMode) {
                chartModeService.chartModes.forEach(function (mode) {
                    if (mode.val === newMode) {
                        chartListService.modeChanged(mode);
                    }
                });
            });

            // Init chart mode
            chart.mode(chartModeService.selectedChartMode.val);

            // Chart mode change
            scope.$on('chartModeChanged', function(event, mode) {
                chart.mode(mode.val);
            });

            /*
             *  Chart Controls
             */
            scope.$watch('control.timeAverage', function () {
                if (scope.running) { return; }
                chart.smoothingPath(scope.control.timeAverage);
            });
            scope.$watch('control.traceSettings', function () {
                chartElement.call(chart.showTraces(scope.control.traceSettings));
            }, true);
        }
    };
});

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 = 800, height = 120; //550

        // Default margin
        var margin = { top: 5, right: 20, bottom: 35, left: 55 };

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

        // 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 showTrace = function (i) {
            return chartTraces.length == 0 || (chartTraces[i] && chartTraces[i].show);
        };

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

        var legendText = function (d, i) {
            if (i === 0) {
                return 'ν: ' + d.toFixed(3) + ' Hz';
            }
            if (!showTrace(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]);

            yAxis.ticks(5);

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

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

        var redrawFft = function () {

            if (samplingRate === null || !spectrums) {
                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 (showTrace(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)
                    .append('g').attr('class', 'x-axis-legend')
                    .append('text').text('Frequency (1/s)')
                    .attr({'text-anchor': 'end', 'alignment-baseline': 'middle'})
                    .style({'font-size': 12});


                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 (!showTrace(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.showTraces = function (_x) {
            if (!arguments.length) {
                return chartTraces;
            }
            chartTraces = _x;
            redrawFft();
            return this;
        };

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

    return fft;
});

angular.module('IOLabApp').directive('iolabFft', function ($rootScope, fftService) {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        require: '^iolabChart',
        templateUrl: 'components/chart.fft.tpl.html',
        link: function (scope, element, attrs, chartCtrl) {

            var fft = fftService(scope.sensor);


            // link the service to the dom element
            fft(d3.select(element[0]));

            fft.width(element.parent()[0].clientWidth);

            // Register fft.redraw callback to update data when they are available
            // in iolab-chart directive
            chartCtrl.fftRedraw = fft.redraw;

            scope.$on('$destroy', function () {
                chartCtrl.fftResult = null;
            });

            /*
             *  Resize event handler
             */
            scope.$on('recompute-chart-height', function () {
                fft.width(element.parent()[0].clientWidth);
            });

            scope.$watch('control.fftSampleRate', function () {
                fft.samplingRate(scope.control.fftSampleRate);
            });

            scope.$watch('control.traceSettings', function () {
                fft.showTraces(scope.control.traceSettings);
            }, true);
        }
    };
});

angular.module('IOLabApp').factory('chartListService', function ($rootScope, $timeout, IOLab, sensorSelectorService, chartModeService, configService) {
    'use strict';

    var chartToDisplay = 0,
        exports = {},
        sensors = {},
        ecgSensors = [],
        chartSensors = [],
        parametricPlots = [];

    // List of sensors to display. Contains identification variables (remote, config and type)
    sensors[1] = [];
    sensors[2] = [];

    // Register the update sensor callback in sensorSelector service
    sensorSelectorService.registerUpdateChartListCallback(updateChartList);

    function updateChartList(remote, reset = false) {
        var acquisition = IOLab.acquisition(remote);
        var acquisitionSensors = acquisition.sensors;
        var updates = {
            reset: reset,
            remote: remote,
            add: [],
            remove: []
        };

        if(reset) {
            sensors[remote] = [];
        }

        _.forEach(acquisitionSensors, function (acqSensor) {
            // Index of the sensor in the scope list
            var sensor = _.find(sensors[remote], { sensorType: acqSensor.sensorType }),
                checked = sensorSelectorService.isSensorChecked(remote, acqSensor.sensorType),
                sensorParam,
                sensorToRemove;
            
            // Add sensor to the sensor list
            if (checked) {
                sensorParam = configService.getSensor(acqSensor.sensorType);
                acqSensor.desc = sensorParam.desc;
                acqSensor.shortDesc = sensorParam.shortDesc;

                if (sensorParam.subCharts) {
                    _.forEach(sensorParam.subCharts, function(subChart, coordinate) {
                        var subSensorChecked = sensorSelectorService.isSensorChecked(remote, acqSensor.sensorType, coordinate);
                        var subSensor = _.find(sensors[remote], { sensorType: acqSensor.sensorType, coordinate: coordinate });

                        // Add subSensor
                        if (subSensorChecked && !subSensor) {
                            var newSensor = angular.copy(acqSensor);
                            newSensor.subDesc = (sensorParam.subCharts[coordinate] || {}).desc;
                            newSensor.coordinate = coordinate;
                            newSensor.sampleSize = 1;
                            sensors[remote].push(newSensor);

                            updates.add.push(newSensor);
                        }

                        // Remove subSensor
                        if (!subSensorChecked && subSensor) {
                            sensorToRemove = _.remove(sensors[remote],
                                s => (s.sensorType == acqSensor.sensorType && s.coordinate == coordinate));

                            updates.remove = updates.remove.concat(sensorToRemove);
                        }
                    });
                }
                else if (!sensor) {
                    sensors[remote].push(acqSensor);
                    updates.add.push(acqSensor);
                }
            }
            // Remove sensor from the sensor list
            if (!checked && sensor) {
                sensorToRemove = _.remove(sensors[remote],
                    s => s.sensorType == acqSensor.sensorType);

                updates.remove = updates.remove.concat(sensorToRemove);
            }
        });

        if (exports.chartType == 'chart') {
            parametricPlots[remote] = [];
            // ECG9 ?
            if (_.find(sensors[remote], { sensorType: 241 })) {
                chartSensors[remote] = [];
                ecgSensors[remote] = sensors[remote];
            } else {
                chartSensors[remote] = sensors[remote];
                ecgSensors[remote] = [];
            }
        }
        
        if (exports.chartType == 'paramPlot') {
            chartSensors[remote] = [];

            if (sensors[remote].length > 0) {
                parametricPlots[remote] = [{
                    remote: remote,
                    config: acquisition.config,
                    sensor1: sensors[remote][0],
                    sensor2: sensors[remote][1] || sensors[remote][0]
                }];
            } else  {
                parametricPlots[remote] = [];
            }
        }

        $timeout(function() {
            // To avoid error when chart directive is destroyed
            $rootScope.$broadcast('chart-list-changed', updates);

            $timeout(function() {$rootScope.$broadcast('recompute-chart-height');});
        }, 100);
    }

    // Look for fixed configuration change
    $rootScope.$on('acquisitionStackChanged', function(event, newAcq, oldAcq) {
        var newRemote1Config = (newAcq['remote1'] || {}).config || 0,
            newRemote2Config = (newAcq['remote2'] || {}).config || 0,
            oldRemote1Config = (oldAcq['remote1'] || {}).config || 0,
            oldRemote2Config = (oldAcq['remote2'] || {}).config || 0;
        
        if (newRemote1Config !== oldRemote1Config) {
            updateChartList(1, true);
        }
        if (newRemote2Config !== oldRemote2Config) {
            updateChartList(2, true);
        }
    });

    exports.getChartLists = function () {
        return {
            ecgSensors: ecgSensors,
            chartSensors: chartSensors,
            parametricPlots: parametricPlots
        };
    };

    //
    // Chart type centralized in chartList service
    //
    exports.chartType = 'chart';
    exports.typeChanged = function () {
        if (sensors[1].length) { updateChartList(1); }
        if (sensors[2].length) { updateChartList(2); }
    };
    
    //
    // Chart Height calculation
    //
    exports.setChartToDisplay = function(val) {
        chartToDisplay = val;
        $rootScope.$broadcast('recompute-chart-height');
    };

    exports.getChartToDisplay = function() {
        return chartToDisplay;
    };
    
    exports.getSvgHeight = function(remote) {
        var defaultChartHeight = 250,
            headerHeight = 120,
            availableHeight = window.innerHeight - headerHeight,
            totalNumberOfChart = chartSensors[remote].length,
            svgHeight = availableHeight / (chartToDisplay || totalNumberOfChart) - 45;

        // In mode "auto", max height = 250
        if (chartToDisplay == 0 && svgHeight > defaultChartHeight) {
            return defaultChartHeight;
        }

        // Minimal height: 120
        return Math.max(svgHeight, 120);
    };

    //
    // Initialisation
    //

    if (IOLab.acquisition(1).config || 0) {
        updateChartList(1);
    }
    if (IOLab.acquisition(2).config || 0) {
        updateChartList(2);
    }

    return exports;
});

angular.module('IOLabApp').directive('chartList', function (chartListService, Logger) {
        'use strict';

        return {
            restrict: 'E',
            replace: true,
            scope: true,
            templateUrl: 'components/chart.list.tpl.html',
            link: function (scope, element, attrs) {

                scope.remote = parseInt(attrs.remote, 10);

                scope.$on('chart-list-changed', function() {
                    var sensorLists = chartListService.getChartLists();
                    scope.ecgSensors = sensorLists.ecgSensors;
                    scope.chartSensors = sensorLists.chartSensors;
                    scope.parametricPlots = sensorLists.parametricPlots;
                });
            }
        };
    });

angular.module('IOLabApp').factory('parametricPlotService', 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,

            // Number of coordinate for the sensor (accelerometer as 3 coordinates)
            sensorDataLen = {
                1: args.sensor1.sampleSize,
                2: args.sensor2.sampleSize
            },

            // Coordinate to be displayed ('-1' for the norm)
            sensorCoord = {
                1: -1,
                2: -1
            },

            // Sub chart coordinate (in case of split chart as the wheel)
            subChartCoord = {
                1: args.sensor1.coordinate,
                2: args.sensor2.coordinate
            },

            // Sensor parameters (from config)
            param = {
                1: configService.getSensor(sensor1) || {},
                2: configService.getSensor(sensor2) || {}
            },
            // Subchart parameters if any (split charts as the wheel)
            subChart = {
                1: param[1].subCharts ? param[1].subCharts[subChartCoord[1]] : undefined,
                2: param[2].subCharts ? param[2].subCharts[subChartCoord[2]] : undefined
            };

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

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

        // Chart scales' range
        var defScale = {};

        //  Set up scales which can depend on sensor coordinates if any
        _.forEach([1, 2], function(axis) {
            var c0 = axis == 1 ? 'x0' : 'y0',
                c1 = axis == 1 ? 'x1' : 'y1';

            if(param[axis].scales){
                defScale[c0] = param[axis].scales[0];
                defScale[c1] = param[axis].scales[1];
            } else if (subChart[axis] && subChart[axis].scales) {
                defScale[c0] = subChart[axis].scales[0];
                defScale[c1] = subChart[axis].scales[1];
            } else {
                defScale[c0] = -20;
                defScale[c1] = 20;
            }
        });

        var scale = Object.create(defScale);
        var scales = [];

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

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

            d3.select('body')
                .on('mouseup.resize', resize.mouseup)
                .on('mousemove.resize', resize.mousemove);
        };
        resize.mouseup = function () {
            resize.active = false;
            resize.d3MouseDown = null;

            d3.select('body')
                .on('mouseup.resize', null)
                .on('mousemove.resize', null);
        };
        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;
            if (height < 200 || width < 200) return;
            redraw();
            brush.extent(brush.extent());
        };

        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 || chartMode.zoom_x || chartMode.zoom_y) {
                    redrawZoomSelection();
                }
            }

            // FIXME - to check
            if (chartMode.stats || chartMode.zoom || chartMode.zoom_x || chartMode.zoom_y || 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 || chartMode.zoom_x || chartMode.zoom_y) {
                redrawZoomSelection();
            }
        };

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

        var mouseup = function () {
            // Set the new scale in zoom mode
            if ((chartMode.zoom || chartMode.zoom_x || chartMode.zoom_y) && selection.notNull()) {
                // Keep the old scale for further use
                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 = scales.length;
            if ((chartMode.zoom || chartMode.zoom_x || chartMode.zoom_y) && !!scalesLen && d3MouseDown[0] === d3.mouse(this)[0]) {
                scale = scales.splice(-1, 1)[0];
                redraw();
                redrawZoomSelection();
            }
        };

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

        var redrawZoomSelection = function () {

            /*
             * Cross
             */
            var crossData = [],
                crossGroup;

            var legend = [],
                legendGroup;

            if ((chartMode.zoom || chartMode.zoom_x || chartMode.zoom_y) && d3Mouse && !selection.p0.locked) {
                crossData = [d3Mouse[0], d3Mouse[1]];
            }

            if ((chartMode.zoom || chartMode.zoom_x || chartMode.zoom_y) && d3Mouse) {
                var x = xScale.invert(d3Mouse[0]);
                var y = yScale.invert(d3Mouse[1]);
                legend = [x, y];
            }

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


            // Update/create the sensor values
            legendGroup = svg.select('.zoom-group')
                .selectAll('.zoom-text')
                .data(legend)
                .attr('x', 10)
                .attr('y', function (d, i) {
                    return 0 + i * 13;
                })
                .text(function (d, i) {
                    return ['x: ', 'y: '][i]
                        + d.toFixed(4) + ' '
                        + (subChart[i + 1] || param[i + 1] || {}).unit  || '';
                });
            legendGroup.enter()
                .append('text')
                .attr('class', 'zoom-text')
                .attr('fill', '#555555');
            legendGroup.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.attr({ viewBox: "0 0 " + width + " " + height });
            svg.attr({ preserveAspectRatio: "xMidYMid meet" });

            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) {
            // axis is 1 (x) or 2 (y).

            // Norm of the vector !
            if (sensorCoord[axis] == -1) {
                var i, len = sensorDataLen[axis], norm2 = 0, start = 1;
                if (axis == 2) { start += sensorDataLen[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 += sensorDataLen[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-holder')
                    .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 + ')' });

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

                var unit2 = ((subChart[2] || param[2] || {}).unit || '-'),
                    subChartDesc2 = subChart[2] ? ' - ' + subChart[2].desc : '';
                container.append('g').classed('y-axis-group axis', true)
                    .append('g').attr('class', 'y-axis-legend')
                    .append('text').text(param[2].desc + subChartDesc2 + ' (' + unit2 + ')')
                    .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);

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

            // Update legend if unit changed...
            var unit = ((subChart[idx] || param[idx] || {}).unit || '-'),
                subChartDesc = subChart[idx] ? ' - ' + subChart[idx].desc : '';
            svg.select('g.' + (idx == 1 ? 'x' : 'y') + '-axis-legend')
                .select('text')
                .text(param[idx].desc + subChartDesc +  ' (' + unit + ')');

            // 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').directive('iolabParametricPlot', function ($rootScope, $timeout, IOLab, parametricPlotService, chartModeService) {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        scope: true,
        require: ['iolabParametricPlot', '?^slideShow'],
        controller: 'parametricPlotCtrl',
        templateUrl: 'components/parametric.plot.tpl.html',
        link: function (scope, element, attrs, ctrls) {

            var remote = scope.plotParams.remote,
                sensor1 = scope.plotParams.sensor1.sensorType,
                sensor2 = scope.plotParams.sensor2.sensorType,

                coord1 = scope.plotParams.sensor1.coordinate,
                coord2 = scope.plotParams.sensor2.coordinate,

                paramPlotCtrl = ctrls[0],
                slideShowCtrl = ctrls[1];

            // D3 class object
            var paramPlot = parametricPlotService(scope.plotParams);

            // html chart element
            var chartElement = d3.select(element[0]);

            // Set height & width
            paramPlot.height(350);
            paramPlot.width(element.parent()[0].clientWidth - (slideShowCtrl ? 20 : 0));


            // Draw the chart element
            chartElement.call(paramPlot);  // idem 'chart(chartElement);'
            paramPlot(chartElement); // idem chartElement.call(chart);

            var timeoutId,
                startTime = 0;

            // Used to compute loading time
            var t1, t2, t3;

            function updateChart() {
                if (scope.running) {
                    timeoutId = $timeout(function () {
                        IOLab.getSensorData2(
                            addDataToChart,
                            {
                                remote: remote,
                                sensor1: sensor1,
                                sensor2: sensor2,
                                startTime: startTime,
                                coordinate1: coord1, 
                                coordinate2: coord2
                            }
                        );
                    }, 20, false);
                } else {
                    console.log('*** Force cancel timeout !!! ***');
                    $timeout.cancel(timeoutId);
                }
            }

            function addDataToChart(json) {
                var newData = json;

                // if data ...
                if (!!newData.data && !!newData.data.length) {

                    // Keep the start time for the next data query
                    var lastTimeIndex = Math.max(newData.data.length - 1, 0);
                    startTime =  newData.data[lastTimeIndex][0];

                    // Draw a new path
                    paramPlot.addData(newData.data);
                }

                updateChart();
            }

            function loadDataToChart(json) {
                var newData = json;
                t2 = new Date();
                console.log('*** stopDataCallBack - getSensorData:', t2 - t1, 'ms');
                if (newData.datalength === 0) {
                    scope.alert.loading = false;
                    scope.alert.error = true;
                    scope.$$phase || scope.$apply();
                    return;
                }

                paramPlot.removeData();
                paramPlot.addData(newData.data);
                paramPlot.showTimeScale(true);
                scope.alert.loading = false;
                scope.$$phase || scope.$apply();
            }

            // Load the data if any when the chart is created !
            if (!IOLab.isNewAcquisition()) {
                if (scope.running) {
                    updateChart();
                } else {
                    t1 = new Date();
                    scope.alert.loading = true;
                    IOLab.getSensorData2(loadDataToChart, { remote: remote, sensor1: sensor1, sensor2: sensor2, coordinate1: coord1, coordinate2: coord2 });
                }
            }

            scope.$on('dataStarted', function () {
                console.log('*** startData ***');
                scope.alert.error = false;
                scope.alert.loading = false;
                scope.running = true;

                paramPlot.showTimeScale(false);
                updateChart();
            });

            scope.$on('dataStopped', function () {
                console.log('*** stopData ***');
                scope.running = false;

                paramPlot.showTimeScale(true);
                $timeout.cancel(timeoutId);
            });

            scope.$on('$destroy', function () {
                console.log('*** scope.$on => destroy iolabParametricPlot ***');

                if (slideShowCtrl) {
                    slideShowCtrl.unregisterParamPlot(scope);
                }

                paramPlot.clean();
            });

            element.bind('$destroy', function () {
                $timeout.cancel(timeoutId);
                unRegister();
            });

            /*
             *  Resize event handler
             */
            var unRegister = $rootScope.$on('recompute-chart-height', function () {

                // Set new values
                paramPlot.height(350);
                paramPlot.width(element.parent()[0].clientWidth - (slideShowCtrl ? 20 : 0));

                // Redraw the chart
                paramPlot();
            });

            // Init chart mode
            paramPlot.mode(chartModeService.selectedChartMode.val);

            // Chart mode change
            scope.$on('chartModeChanged', function(event, mode) {
                paramPlot.mode(mode.val);
            });

            /*
             *  Plots Controls
             */
            scope.$watch('control.coordinate1', function (newVal) {
                var index = scope.coordinates1.indexOf(newVal);
                if (scope.coordinates1[0] == 'Norm') { index--; } // Index = -1 if Norm!!!
                paramPlot.coordinate(1, index);
                paramPlot();
            });
            scope.$watch('control.coordinate2', function (newVal) {
                var index = scope.coordinates2.indexOf(newVal);
                if (scope.coordinates2[0] == 'Norm') { index--; } // Index = -1 if Norm!!!
                paramPlot.coordinate(2, index);
                paramPlot();
            });
            scope.$watch('control.timeRange', function (newVal) {
                paramPlot.timeRange(newVal);
                paramPlot();
            });

            //
            // Slide show management
            //
            if (slideShowCtrl) {
                slideShowCtrl.registerParamPlot(scope);
            }
        }
    };
});

angular.module('IOLabApp').controller('parametricPlotCtrl', function ($scope, IOLab, configService) {
    'use strict';

    $scope.iolabState = IOLab.hardwareState();

    var remote = $scope.plotParams.remote,
        config = $scope.plotParams.config,
        sensor1 = $scope.plotParams.sensor1,
        sensor2 = $scope.plotParams.sensor2,
        param1 = configService.getSensor(sensor1.sensorType) || {},
        param2 = configService.getSensor(sensor2.sensorType) || {};

    $scope.open = { tools: false };
    $scope.alert = {};

    // Drop down for the coordinate selection
    // ($scope.coordinates1 contains the list of coordinate lable as 'm/s', ...)
    var coordinates1 = (param1.subCharts ? param1.subCharts[sensor1.coordinate] : param1).legends || [];
    $scope.coordinates1 = angular.copy(coordinates1);
    if (coordinates1.length > 1 && coordinates1[0][0] == coordinates1[1][0]) {
        $scope.coordinates1.unshift('Norm');
    }

    var coordinates2 = (param2.subCharts ? param2.subCharts[sensor2.coordinate] : param2).legends || [];
    $scope.coordinates2 = angular.copy(coordinates2);
    if (coordinates2.length > 1 && coordinates2[0][0] == coordinates2[1][0]) {
        $scope.coordinates2.unshift('Norm');
    }

    // Drop down for time range selection
    $scope.timeRanges = [50, 100, 250, 500, 1000, 2000, 5000];

    $scope.running = $scope.iolabState.dongleMode === 16;
    $scope.frequency = 100;
    $scope.sensor1 = sensor1;
    $scope.sensor2 = sensor2;

    $scope.control = {
        chartMode: $scope.chartModes[1],
        coordinate1: $scope.coordinates1[0],
        coordinate2: $scope.coordinates2[0],
        timeRange: $scope.timeRanges[0]
    };
});

angular.module('IOLabApp')
    .service('snapshotService', function($rootScope, $q, $timeout, IOLab, mgtCMD) {
        'use strict';

        var chartScopes = [];
        var self = this;

        self.selectedSnapshotDate = null;
        self.updateSelectedSnapshot = function(selectedSnapshotDate){
            self.selectedSnapshotDate = selectedSnapshotDate;
        };

        // When acquisition stack changed get snapshots ...
        $rootScope.$on('acquisitionStackChanged', function(event, newAcq, oldAcq) {
            if (newAcq === oldAcq) { return; }

            init();
        });

        function init() {
            if (IOLab.isNewAcquisition()) {
                self.snapshots = [];
                return;
            }

            var acqId = IOLab.acquisition().ID;
            getSnapshot(acqId)
                .then(function(snapshots) {
                    self.snapshots = snapshots || [];
                });
        }

        function registerChart (scope) {
            chartScopes.push(scope);
        }

        function unregisterChart (scope) {
            _.pull(chartScopes, scope);
        }

        function takeSnapshot () {
            var activeChartScopes = _.filter(chartScopes, function(scope) {
                return scope.showChart;
            });

            if (activeChartScopes.length === 0) {
                return;
            }

            return {
                date: (new Date()).toISOString(),
                sensors: _.map(activeChartScopes, function(scope) {
                    return angular.copy(scope.takeSnapshot());
                })
            };
        }

        function saveSnapshot (additionalData) {

            var acqId = IOLab.acquisition().ID;

            var newSnapshot = takeSnapshot();
            if (!newSnapshot) {
                return $q.when();
            }

            return IOLab.manageAcquisitions(mgtCMD.Get, acqId)
                .then(function(acquisition) {

                    _.extend(newSnapshot, additionalData);

                    if (!acquisition.snapshots) {
                        //  If acquisition.snapshots is undefined it is defined and populated by the latest snapshot
                        acquisition.snapshots = [newSnapshot];
                    } else if (!acquisition.snapshots.length || !self.selectedSnapshotDate){
                        //  If acquisition.snapshots is empty or if no snapshot is selected the latest snapshot is added to acquisition.snapshots
                        acquisition.snapshots.push(newSnapshot);
                    }

                    return IOLab.manageAcquisitions(mgtCMD.Save, acqId, acquisition)
                        .then(function() {
                            self.snapshots = acquisition.snapshots;
                            return newSnapshot;
                        });
                });
        }

        // Get snapshot store in acquisition meta data (acquisition mode)
        function getSnapshot (acqId, query) {

            if (!acqId) {
                return $q.when();
            }

            return $q.when()
                .then(function() {
                    return IOLab.manageAcquisitions(mgtCMD.Get, acqId);
                })
                .then(function(acquisition) {

                    if (query) {
                        return _.find(acquisition.snapshots, query);
                    }

                    return acquisition.snapshots;
                });
        }

        //  Get all existing snapshots
        function getSnapshots () {
            return getSnapshot(IOLab.acquisition().ID);
        }

        function reloadAcquisition (acqID) {
            if (acqID === IOLab.acquisition().ID) {
                return $q.when();
            }

            var deferred = $q.defer();

            $q.when()
                .then(function() {
                    return IOLab.resetAcquisition();
                })
                .then(function() {
                    return IOLab.manageAcquisitions(mgtCMD.Reload, acqID);
                })
                .then(function() {
                    $timeout(deferred.resolve, 150);
                });

            return deferred.promise;
        }

        function reloadAcquisitionSet (acqSet) {
            var apiAcqSet = IOLab.acquisitionSet(),
                apiAcqIDs = _.map(apiAcqSet.runs, 'acqID' ),
                slideAcqIDs = _.map(acqSet.runs, 'acqID' ),
                apiConfig1 = (apiAcqSet.fixedConfig || {})[1],
                slideConfig1 = (acqSet.fixedConfig || {})[1];
            
            if (_.isEqual(slideAcqIDs, apiAcqIDs) && apiConfig1 == slideConfig1) {
                return $q.when({ resetAllCharts: false });
            }

            var promiseQueue = IOLab.resetAcquisition();

            // Set fixedConfig if needed
            var remote = 1,
                config = (acqSet.fixedConfig || {})[remote];
            
            // FIXME - Check that the remote is powered on !!! 
            // FIXME - This code need to be moved in the player...
            if (acqSet.runs.length == 0 && config) {
                promiseQueue = promiseQueue.then(function() {
                    var deferred = $q.defer();
                    if (IOLab.hardwareState(remote).fixedConfig == config) {
                        return deferred.resolve();
                    }

                    return IOLab.setFixedConfig(remote, config)
                        .then(function () {
                            var cancel = $rootScope.$on('hardwareStateChanged', function () {
                                cancel();
                                deferred.resolve();
                            });
                        });
                });
            }
            
            _.forEach(acqSet.runs, function(run) {
                promiseQueue = promiseQueue
                    .then(function() {
                        return IOLab.newAcquisitionSetRun();
                    })
                    .then(function() {
                        return IOLab.manageAcquisitions(mgtCMD.Reload, run.acqID);
                    })
                    .then(function() {
                        return IOLab.getAcquisition();
                    });
            });

            promiseQueue = promiseQueue
                .then(function() {
                    // Must be done after the IOLab.resetAcquisition()
                    // since this command resets also the acquisitionSet ...
                    IOLab.setAcquisitionSet(acqSet);
                    
                    return { resetAllCharts: true };
                });
            
            return promiseQueue;
        }

        function restoreSnapshot (snapshot) {

            if (!snapshot) {
                _.forEach(chartScopes, function(scope) {
                    scope.showChart = false;
                });
                return;
            }

            _.forEach(chartScopes, function(chartScope) {
                var sensor = chartScope.sensor.sensorType,
                    remote = chartScope.sensor.remote,
                    coordinate = chartScope.sensor.coordinate;

                var sensorSnapshot = _.find(snapshot.sensors, function(sn) {
                    return sn.sensor === sensor &&
                        sn.remote === remote &&
                        sn.coordinate === coordinate;
                });

                if (!sensorSnapshot) {
                    chartScope.showChart = false;
                    return;
                }
                chartScope.showChart = true;

                // Control (can be set directly)
                chartScope.control.reverseAxis = sensorSnapshot.control.reverseAxis;
                chartScope.control.timeAverage = sensorSnapshot.control.timeAverage;
                _.forEach(chartScope.control.traceSettings, function(traceSetting, idx) {
                    traceSetting.show = _.get(sensorSnapshot, 'control.tracesShown[' + idx + ']');
                });

                // Set scale + Cursor range once the chart is loaded
                var scale = sensorSnapshot.scale,
                    defScale = sensorSnapshot.defScale,
                    cursorRange = sensorSnapshot.cursorRange || {t0: -1, t1: -1};

                chartScope.restoreSnapshot(scale, defScale, cursorRange);
            });

            //  Remember the selectedSnapshotDate
            self.selectedSnapshotDate = snapshot.date;
        }

        function deleteSnapshot (snapshot) {
            var acqId = IOLab.acquisition().ID;

            return IOLab.manageAcquisitions(mgtCMD.Get, acqId)
                .then(function(acquisition) {
                    var removedSnapshot = _.remove(acquisition.snapshots, function(sn) {
                        return sn.date == snapshot.date;
                    });

                    if (!removedSnapshot) {
                        console.log('deleteSnapshot - snapshot not found');
                        return $q.reject('Snapshot not found in the acquisition data');
                    }

                    return IOLab.manageAcquisitions(mgtCMD.Save, acqId, acquisition)
                        .then(function() {
                            self.snapshots = acquisition.snapshots;

                            //  Reset the selectedSnapshotDate if any and if it corresponds to the deleted snapshot
                            if(self.selectedSnapshotDate && self.selectedSnapshotDate == snapshot.date){
                                self.selectedSnapshotDate = null;
                            }
                        });
                });
        }

        //
        // Exported functions
        //

        this.snapshot = [];

        this.takeSnapshot = takeSnapshot;
        this.saveSnapshot = saveSnapshot;
        this.getSnapshot = getSnapshot;
        this.getSnapshots = getSnapshots;
        this.deleteSnapshot = deleteSnapshot;

        this.restoreSnapshot = restoreSnapshot;
        this.reloadAcquisition = reloadAcquisition;
        this.reloadAcquisitionSet = reloadAcquisitionSet;

        this.registerChart = registerChart;
        this.unregisterChart = unregisterChart;

        init();
    });


angular.module('IOLabApp')
    .directive('acquisitionSnapshot', function($rootScope, $timeout, IOLab, snapshotService, sensorSelectorService) {
        'use strict';

        return {
            restrict: 'E',
            scope: true,
            template:
                '<div ng-hide="IOLab.acquisitionSet().runs.length == 0" class="snapshot">' +
                '   <h4 class="no-print hidden-xs">Snapshots</h4>' +
                '   <a href="" ng-if="!params.takeSnapshot" ng-click="params.takeSnapshot = true" class="link-action">Take a new snapshot</a>' +
                '   <div ng-if="params.takeSnapshot" class="params">' +
                '       <textarea type="text" ng-model="params.comment" placeholder="Comment" class="snapshot-textarea"></textarea>' +
                '       <span class="pull-right">' +
                '           <a href="" ng-click="cancelSnapshot()">Cancel</a> |' +
                '           <a href="" ng-click="takeSnapshot()">Save</a>' +
                '       </span>' +
                '   </div>' +
                '   <div class="snapshot-entry pointer"' +
                '        ng-repeat="snapshot in snapshotService.snapshots" ' +
                '        ng-init="show.confirmDelete = false"' +
                '        ng-class="{ selected: snapshot.date == selectedSnapshotDate }" ' +
                '        ng-click="restoreSnapshot(snapshot)">' +
                '       <span>{{snapshot.date | date:\'medium\'}}</span> ' +
                '       <span ng-if="!show.confirmDelete">' +
                '           <a href="" ng-click="show.confirmDelete = true; $event.stopPropagation();">[Delete]</a>' +
                '       </span>' +
                '       <span ng-if="show.confirmDelete">' +
                '           <a href="" ng-click="show.confirmDelete = false; $event.stopPropagation();">[Cancel]</a>' +
                '           <a href="" ng-click="deleteSnapshot(snapshot); $event.stopPropagation();" style="color: darkred;">[Confirm]</a>' +
                '       </span>' +
                '       <br/>' +
                '       <span class="snapshot-comment">{{snapshot.comment}}</span> &nbsp; ' +
                '   </div>' +
                '</div>',
            link: function(scope) {

                scope.IOLab = IOLab;
                scope.snapshotService = snapshotService;

                function watchSnapshot () {
                    var cancelWatch = scope.$watch(
                        function() {
                            return (snapshotService.takeSnapshot() || {}).sensors;
                        },
                        function(newVal, oldVal) {
                            if (_.isEqual(newVal, oldVal)) return;
                            scope.selectedSnapshotDate = null;
                            snapshotService.updateSelectedSnapshot(scope.selectedSnapshotDate);
                            cancelWatch();
                        },
                        true
                    );
                }

                scope.params = {};
                scope.takeSnapshot = function () {
                    snapshotService.saveSnapshot({
                        comment: scope.params.comment
                    })
                    .then(function(snapshot) {
                        scope.selectedSnapshotDate = snapshot.date;
                        snapshotService.updateSelectedSnapshot(scope.selectedSnapshotDate);
                        scope.params = {};
                        watchSnapshot();
                    });
                };

                scope.restoreSnapshot = function (snapshot) {

                    _.forEach([1, 2], function(remote) {

                        _.forEach(sensorSelectorService.sensors[remote], function(selectionSensor) {

                            var selectionSensorArray;

                            // selectionSensor.subCharts contains "coordinate" and info
                            // about "checked" status for subCharts
                            if (selectionSensor.subCharts) {
                                selectionSensorArray = _.map(selectionSensor.subCharts, function(subchart) {
                                    return {
                                        code: selectionSensor.code,
                                        checked: subchart.checked,
                                        coordinate: subchart.coordinate
                                    };
                                });
                            } else {
                                selectionSensorArray = [{
                                    code: selectionSensor.code,
                                    checked: selectionSensor.checked
                                }];
                            }

                            _.forEach(selectionSensorArray, function(selectionSensor_) {

                                var snapshotSensor = _.find(snapshot.sensors, function(sensor) {
                                    return remote === sensor.remote &&
                                        selectionSensor_.code === sensor.sensor &&
                                        selectionSensor_.coordinate === sensor.coordinate;
                                });

                                if ( ( snapshotSensor && !selectionSensor_.checked) ||
                                     (!snapshotSensor &&  selectionSensor_.checked) ) {
                                    sensorSelectorService.sensorChecked(remote, selectionSensor, selectionSensor_.coordinate);
                                }
                            });
                        });
                    });

                    $timeout(function() {
                        snapshotService.restoreSnapshot(snapshot);
                        scope.selectedSnapshotDate = snapshot.date;
                        watchSnapshot();
                    }, 100);
                };

                scope.cancelSnapshot = function () {
                    scope.params = {};
                };

                scope.deleteSnapshot = function (snapshot) {
                    snapshotService.deleteSnapshot(snapshot);
                };
            }
        };
    });

angular.module('IOLabApp')
    .directive('acquisitionSet', function($rootScope, IOLab) {
        'use strict';

        return {
            restrict: 'E',
            scope: true,
            template:
                '<div class="acquisition-set"> ' +
                '   <h4 class="no-print hidden-xs">Data sets</h4>' +
                '   <span style="font-weight: bold;">Fixed duration:</span>' +
                '   <input type="number" placeholder="none" style="width: 40px; padding: 2px 4px; border: 1px solid #ccc; border-radius: 3px;"' +
                '          ng-model="IOLab.acquisitionSet().duration"/> ' +
                '   <span style="font-weight: bold;">(s)</span>' +
                '   <div ng-repeat="run in IOLab.acquisitionSet().runs" class="entry">' +
                '       <span>{{run.name}}</span> ' +
                '       <a href="#" ng-click="IOLab.removeAcquisitionSetRun(run)">[Remove]</a></span> ' +
                '       <br/>' +
                '       <small class="acq-ID">ID: {{run.acqID}}</small>' +
                '   </div>' +
                '</div>',
            link: function(scope) {

                scope.IOLab = IOLab;
            }
        };
    });

angular.module('IOLabApp').service('IOLabVersion', function($http) {
    'use strict';

    const remote = require('electron').remote;

    var version = remote.getGlobal('iolabVersion');

    this.value = version;
    this.appTitle = 'IOLab (' + version + ')';

    this.checkVersion = function() {

        return $http.get('http://www.iolab.science/iolab.version.txt?' + (new Date()).toISOString())
            .then(function(res) {
                var result = res.data;

                var newVersionSplit = (result.last_version || '').split('.'),
                    currentVersionSplit = version.split('.');

                result.newVersion =
                    newVersionSplit[2] > currentVersionSplit[2] ||
                    newVersionSplit[1] > currentVersionSplit[1] ||
                    newVersionSplit[0] > currentVersionSplit[0];

                return result;
            });
    }

});


angular.module('IOLabApp').directive('newVersion', function($rootScope, $timeout, $http, IOLabVersion) {
    "use strict";

    return {
        scope: true,
        restrict: 'E',
        template:
            '<div ng-if="result.newVersion">' +
            '   <alert type="info" style="font-size: 110%;">' +
            '       <a href="#" class="close" style="line-height: 0.5" ng-click="close()">&times;</a>' +
            '       <div style="margin-bottom: 5px;">' +
                        '<span ng-bind-html="result.description_1_part1"></span>' +
                        '<a ng-if="result.description_1_link" href="" ng-click="open()"><span ng-bind-html="result.link_label"></span></a>' +
                        '<span ng-bind-html="result.description_1_part2"></span>' +
            '       </div>' +
            '       <div>' +
                        '<span ng-bind-html="result.description_2_part1"></span>' +
                        '<a ng-if="result.description_2_link" href="" ng-click="open()"><span ng-bind-html="result.link_label"></span></a>' +
                        '<span ng-bind-html="result.description_2_part2"></span>' +
            '       </div>' +
            '   </alert>' +
            '</div>',
        link: function(scope, element) {

            var mainPageAlert = element[0].className.indexOf('body') > -1;

            scope.open = function() {
                require('electron').shell.openExternal(scope.result.link_url);
            };

            scope.close = function() {
                scope.result.newVersion = false;

                if (mainPageAlert) {
                    $rootScope.controlExtraHeight = {height: '0px'};
                }
            };

            IOLabVersion.checkVersion()
                .then(function(result) {

                    scope.result = result;

                    // Replace the last_version string by the value
                    _.forEach(result, function(value, key) {
                        if (key.indexOf('description') > -1 && value.replace ) {

                            var values = value
                                .replace('{last_version}', result.last_version)
                                .split('{link}');

                            result[key + '_part1'] = values[0];
                            result[key + '_part2'] = values[1];
                            result[key + '_link']  = value.indexOf('{link}') > -1;
                        }
                    });

                    if (mainPageAlert) {
                        $timeout(function() {
                            var div = element.find('DIV')[1] || {},
                                offset = div.offsetHeight || 0;
                            $rootScope.controlExtraHeight = { height: offset + 'px' }
                        });
                    }

                });
        }
    }

});
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.resetData = function (data) {

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

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

    exports.smoothData = function (data, delta) {
        var i, j, n, sum;

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

        for (n = 0; n < data.length; n++) {
            for (i = 0; i < delta; i++) {
                for (sum = 0, j = 0; j < i + delta + 1; j++) {
                    sum += data[n][j][2];
                }
                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 += data[n][j][2];
                }
                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 += data[n][j][2];
                }
                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.linearRegression = function(data) {

            var lr = {};
            var n = data.length;
            var sum_x = 0;
            var sum_y = 0;
            var sum_xy = 0;
            var sum_xx = 0;
            var sum_yy = 0;

            for (var i = 0; i < data.length; i++) {
                sum_x += data[i][0];
                sum_y += data[i][1];
                sum_xy += (data[i][0] * data[i][1]);
                sum_xx += (data[i][0] * data[i][0]);
                sum_yy += (data[i][1] * data[i][1]);
            }

            lr['slope'] = (n * sum_xy - sum_x * sum_y) / (n*sum_xx - sum_x * sum_x);
            lr['intercept'] = (sum_y - lr.slope * sum_x) / n;
            lr['r2'] = Math.pow((n * sum_xy - sum_x * sum_y) / Math.sqrt((n * sum_xx - sum_x * sum_x) * (n * sum_yy - sum_y * sum_y)), 2);

            return lr;
    };

    exports.computeScale = function (data, scale, bbox, parityFactors) {

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

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

        // x scale ...
        var deltaScaleX = scale.x1 - scale.x0,
            delta_t = data[0][1][0] - data[0][0][0];

        res.x0 = data[0][0][0] - delta_t;
        res.x1 = data[0][len - 1][0] > deltaScaleX ? data[0][len - 1][0]: deltaScaleX;

        // TODO - Check if the trace is shown...
        function scaledBboxFn (key) {
            var key1 = key;
            var key2 = key == 'max' ? 'min' : 'max';

            return function (el, idx) {
                var parity = parityFactors[idx % parityFactors.length];
                if (parity == 1) {
                    return bbox[key1][idx];
                } else {
                    return bbox[key2][idx] * parity;
                }
            }
        }

        // y scale ...
        if (bbox) {
            var dim = bbox.max.length;

            y0 = _.min(_.map(_.range(dim), scaledBboxFn('min')));
            y1 = _.max(_.map(_.range(dim), scaledBboxFn('max')));

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

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

        return res;
    };

    // Deserialize Blob for chart (not for parametric plot)
    // Structure of the blob: [t, cal_x, cal_y, cal_z, (raw_x, raw_y, raw_z)]
    exports.deserializeBlob = function (blob, sampleSize, dataLength, rawData) {

        // Create a view on the float array
        var bufferFloat32 = new Float32Array(blob);

        if (!dataLength) {
            dataLength = bufferFloat32.length / (sampleSize * (rawData ? 2 : 1) + 1);
        }

        // Initialize the data array
        var i, n, data = [];
        for (n = 0; n < sampleSize; n++) {
            data[n] = [];
        }

        // Fill the data array ...
        for (i = 0; i < dataLength; i++) {
            for (n = 0; n < sampleSize; n++) {
                if (!rawData) {
                    data[n].push([
                        bufferFloat32[i],
                        bufferFloat32[(n + 1) * dataLength + i]
                    ]);
                } else {
                    data[n].push([
                        bufferFloat32[i],
                        bufferFloat32[(n + 1) * dataLength + i],
                        bufferFloat32[(n + 1 + sampleSize) * dataLength + i]
                    ]);
                }
            }
        }
        return data;
    };

    // Deserialize Blob for Office table
    // Structure of the blob: [t, cal_x, cal_y, cal_z, (raw_x, raw_y, raw_z)]
    exports.deserializeBlob2 = function (sensors, blobs, scale) {

        // Extend the sensor array with a view on the blob and with the computed dataLength
        _.forEach(sensors, function (sensor) {
            sensor.bufferFloat32 = new Float32Array(blobs[sensor.ID]);
            sensor.dataLength =  sensor.bufferFloat32.length / (sensor.SampleSize + 1);
            console.log('sensor dataLength', sensor.ID, sensor.dataLength);
        });

        // TODO - Check if all dataLength are the same.
        // Otherwise, raise an error
        var dataLength = sensors[0].dataLength;

        // Initialize the data array
        var i, n, t, row, data = [];

        // Fill the data array ...
        for (i = 0; i < dataLength; i++) {

            t = sensors[0].bufferFloat32[i];

            if (scale && (t < scale.x0 || scale.x1 < t)) {
                continue;
            }

            // Time coordinate
            row = [t];

            // Sensors coordinates
            _.forEach(sensors, function (sensor) {
                for (n = 0; n < sensor.SampleSize; n++) {
                    if (sensor.traces[n].show) {
                        row.push(sensor.bufferFloat32[(n + 1) * dataLength + i]);
                    }
                }
            });
            data.push(row);
        }
        return data;
    };

    return exports;
});

angular.module('IOLabApp').factory('utils', function($q, $timeout) {
    "use strict";
    var exports = {};

    exports.wait = function(ms) {
        return function() {
            var deferred = $q.defer();
            $timeout(deferred.resolve, ms);
            return deferred.promise;
        }
    };

    return exports;
})

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

    var exports = function () {

        var chartMode = {
            stats: null,
            move: null,
            zoom: null,
            zoom_x: null,
            zoom_y: null,
            scales: []
        };

        chartMode.set = function (key) {
            chartMode.stats = false;
            chartMode.move = false;
            chartMode.zoom = false;
            chartMode.zoom_x = false;
            chartMode.zoom_y = 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;
    };

    exports.chartModes = [
        { val: 'stats', glyphicon: 'glyphicon-stats' },
        { val: 'zoom',  glyphicon: 'glyphicon-zoom-in' },
        { val: 'zoom_x', glyphicon: 'icon-zoom-x' },
        { val: 'zoom_y', glyphicon: 'icon-zoom-y' },
        { val: 'move',  glyphicon: 'glyphicon-move' }
    ];

    exports.selectedChartMode = {};
    exports.setChartMode = function(mode) {
        exports.selectedChartMode = mode;
    };
    exports.setChartMode(exports.chartModes[0]);
    
    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('sharedChartService', ['$rootScope', function ($rootScope) {
    'use strict';

    var exports = {};
    var sharedDataSetList = {};

    exports.init = function () {
        exports.scale =  null;
        exports.cursorRange = null;
        exports.cursorPosition = null;
    };

    exports.getSharedChartInstance = function (dataSetID) {
        if (!sharedDataSetList[dataSetID]) {
            sharedDataSetList[dataSetID] = {
                scale: null,
                cursorRange: null,
                cursorPosition: null
            }
        }
        return sharedDataSetList[dataSetID];
    };

    exports.init();

    return exports;
}]);

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

    var exports = {};

    exports.getChartSize = function (elementClass) {
        var elemWidth = document.getElementsByClassName(elementClass)[0].clientWidth;
        return elemWidth;
    };

    return exports;
});

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

    // https://github.com/mongodb/js-bson
    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').directive("ngModelOptions", function () {
    return {
        priority: 1,
        restrict: "A",
        //require: "ngModel",
        require: ['ngModel'/*, '^?select'*/],
        link: function (scope, elm, attr, ctrls) {

            function apply() {

                var value;

                switch (tagType) {
                    case "inputradio":
                        if (attr.ngValue) {
                            value = scope.$eval(attr.ngValue);
                        } else {
                            value = elm.val();
                        }
                        break;
                    case "inputcheckbox":
                        value = elm.prop("checked");
                        break;
                    case "select":
                        value = getSelectOption();
                        break;
                    default:
                        value = elm.val();

                        if (!attr.ngTrim || attr.ngTrim.toLowerCase() !== "false") {
                            value = value.replace(/^\s+|\s+$/g, "");
                        }

                }

                scope.$apply(function () {
                    ctrls[0].$setViewValue(value);
                });
            }

            function getSelectOption() {

                if (!attr.ngOptions) {
                    throw "Must have ng-options";
                }

                var match = attr.ngOptions.match(NG_OPTIONS_REGEXP),
                    track = match[8],
                    valuesName = match[7],
                    values = scope.$eval(valuesName),
                    index,
                    val = elm.val();

                // if track by is specified
                // use the track by value as
                // a key to find the index
                if (track) {
                    angular.forEach(values, function (item, i) {
                        if (item[track.replace(/^.*?\./, "")] == val) {
                            index = i;
                        }
                    });
                } else {
                    index = val;
                }

                // value is a property of an object
                if (/.*\..*/.test(match[1])) {
                    return scope.$eval(match[1].replace(/^.*?\./, valuesName + "[" + index + "]."));
                } else { // value is an object
                    return values[index];
                }
            }

            var timer,
                NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
                tagType = elm[0].nodeName.toLowerCase() + (attr.type || ""),
                defaultEvents = [],
                defaults = {
                    debounce: 0,
                    updateOn: "default"
                },
                options = angular.extend(defaults, scope.$eval(attr.ngModelOptions)),
                multiDebounce = typeof options.debounce === "object",
                updateOnSplit = options.updateOn.split(" ");

            if (tagType === "inputtext" || tagType === "textarea") {
                defaultEvents = ["keydown", "input", "change"];
            } else if (tagType === "inputradio" || tagType === "inputcheckbox") {
                defaultEvents = ["click"];
            } else if (tagType === "select") {
                defaultEvents = ["change"];
            } else {
                defaultEvents = ["keydown", "input", "change"];
            }

            // unbind default events to prevent automatic model updates
            angular.forEach(defaultEvents, function (event) {
                // ie8 does not support some events
                // use try catch to mask this
                try { elm.off(event); } catch (e) { }
            });

            // default events
            if (options.updateOn.match(/default/i)) {
                elm.on(defaultEvents.join(" "), function (e) {
                    var delay = options.debounce["default"] || options.debounce || 0;
                    clearTimeout(timer);
                    timer = setTimeout(function () {
                        apply();
                    }, delay);
                });
            }

            // the other events if specified
            angular.forEach(updateOnSplit, function (event) {
                if (event === "default") {
                    return;
                }

                // debounce option can be number or object
                var deb;
                if (multiDebounce) {
                    deb = options.debounce[event];
                } else {
                    deb = options.debounce;
                }

                var delay = deb;
                if (typeof delay === "undefined") {
                    delay = options.debounce["default"] || 0;
                }

                elm.on(event, function (e) {
                    clearTimeout(timer);
                    timer = setTimeout(function () {
                        apply();
                    }, delay);
                });
            });

        }
    };
});

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

    var colorRed = '#BB0000';
    var colorBlue = '#0000BB';
    var colorGreen = '#008800';

    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',
        label: 'a',
        unit: 'm/s²',
        legends: ['Ax', 'Ay', 'Az'],
        pathColors: [colorRed, colorBlue, colorGreen],
        scales: [-20, 20],
        minScalingRate: 5,
        autoScaleY: false
    };

    sensors[2] = {
        code: 2,
        desc: 'Magnetometer',
        shortDesc: 'Magn',
        label: 'B',
        unit: 'µT',
        legends: ['Bx', 'By', 'Bz'],
        pathColors: [colorRed, colorBlue, colorGreen],
        scales: [-150, 150],
        minScalingRate: 5,
        autoScaleY: false
    };

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

    sensors[4] = {
        code: 4,
        desc: 'Barometer',
        shortDesc: 'Baro',
        label: 'P',
        unit: 'kPa',
        legends: ['Pressure'],
        scales: [0, 120],
        autoScaleY: false
    };

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

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

    sensors[8] = {
        code: 8,
        desc: 'Force',
        shortDesc: 'Force',
        label: 'Fᵧ',
        unit: 'N',
        legends: ['Fᵧ'],
        pathColors: [colorBlue],
        scales: [-5, 5],
        autoScaleY: false
    };

    sensors[9] = {
        code: 9,
        desc: 'Wheel',
        shortDesc: 'Wheel',
        subCharts: [
            { desc: 'Position',     label: 'rᵧ', unit: 'm',    legends: ['rᵧ'], autoScaleY: false, pathColors: [colorRed], scales: [-2, 2] },
            { desc: 'Velocity',     label: 'vᵧ', unit: 'm/s',  legends: ['vᵧ'], autoScaleY: false, pathColors: [colorGreen], scales: [-3, 3] },
            { desc: 'Acceleration', label: 'aᵧ', unit: 'm/s²', legends: ['aᵧ'], autoScaleY: false, pathColors: [colorBlue], scales: [-30, 30] }
        ],
        timeAverageIdx: 2,
        reverseAxis: false
    };

    sensors[10] = { // 0x0A
        code: 10,
        desc: 'Electrocardiogram (3)',
        shortDesc: 'ECG3', // ECG (requires plugin)
        label: 'A',
        unit: 'mV',
        legends: ['V1', 'V2', 'V3'],
        pathColors: [colorRed, colorBlue, colorGreen],
        autoScaleY: false
    };

    sensors[11] = { // 0x0B
        code: 11,
        desc: 'Battery',
        shortDesc: 'Bat',
        label: 'T',
        unit: 'V',
        legends: ['Battery'],
        scales: [0, 4],
        autoScaleY: false
    };

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

    sensors[13] = { // 0x0D
        code: 13,
        desc: 'Digital',
        shortDesc: 'Digi',
        scales: [0, 5],
        autoScaleY: false
    };

    sensors[21] = { // 0x15
        code: 21,
        desc: 'Analog 7',
        shortDesc: 'Ana7',
        label: 'A',
        unit: 'V',
        legends: ['Voltage'],
        scales: [0, 4],
        autoScaleY: false
    };

    sensors[22] = { // 0x16
        code: 22,
        desc: 'Analog 8',
        shortDesc: 'Ana8',
        label: 'A',
        unit: 'V',
        legends: ['Voltage'],
        scales: [0, 4],
        autoScaleY: false
    };

    sensors[23] = { // 0x17
        code: 23,
        desc: 'Analog 9',
        shortDesc: 'Ana9',
        label: 'A',
        unit: 'V',
        legends: ['Voltage'],
        scales: [0, 4],
        autoScaleY: false
    };

    sensors[26] = { // 0x1A
        code: 26,
        desc: 'Thermometer',
        shortDesc: 'Therm',
        label: 'T',
        unit: '°C',
        legends: ['Temp.'],
        pathColors: [colorBlue],
        scales: [0, 40],
        autoScaleY: false
    };

    sensors[241] = { // 0xF1 // Electrocardiogram
        code: 241,
        desc: 'Electrocardiogram (9)',
        shortDesc: 'ECG9',
        unit: 'mV',
        legends: ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V3', 'V6'],
        pathColors: ['#819263', '#3ca13b', '#546f6f', '#342fdd', '#49a6ff', '#515095', '#c34947', '#fa3430', '#a73431'],
        timeAverageIdx: 2,
        scales: [-0.5, 0.5],
        minScalingRate: 5,
        autoScaleY: true
    };

    var fixedConfigs = {};
    fixedConfigs[1] = {
        desc: 'Gyroscope',
        highSpeed: false,
        frequencies: [100, 200, 400],
        sensors: [
            { sensorKey: 3, sampleRate: 380 }
        ]
    };

    fixedConfigs[2] = {
        desc: 'Accelerometer',
        highSpeed: false,
        frequencies: [100, 200, 400],
        sensors: [
            { sensorKey: 1, sampleRate: 400 }
        ]
    };

    fixedConfigs[3] = {
        desc: 'Orientation',
        highSpeed: false,
        frequencies: [100],
        sensors: [
            { sensorKey: 1, sampleRate: 100 },
            { sensorKey: 2, sampleRate: 80 },
            { sensorKey: 3, sampleRate: 95 },
            { sensorKey: 12, sampleRate: 100 }
        ]
    };

    fixedConfigs[4] = {
        desc: 'Mini-motion',
        highSpeed: false,
        frequencies: [100, 200],
        sensors: [
            { sensorKey: 1, sampleRate: 200 },
            { sensorKey: 8, sampleRate: 200 },
            { sensorKey: 9, sampleRate: 100 }
        ]
    };

    fixedConfigs[5] = {
        desc: 'Pendulum',
        highSpeed: false,
        frequencies: [100],
        sensors: [
            { sensorKey: 1, sampleRate: 100 },
            { sensorKey: 3, sampleRate: 95 },
            { sensorKey: 8, sampleRate: 100 }
        ]
    };

    fixedConfigs[6] = {
        desc: 'Ambient',
        highSpeed: false,
        frequencies: [50, 100, 200, 400],
        sensors: [
            { sensorKey: 4, sampleRate: 100 },
            { sensorKey: 7, sampleRate: 400 },
            { sensorKey: 11, sampleRate: 50 },
            { sensorKey: 26, sampleRate: 50 }
        ]
    };

    fixedConfigs[7] = {
        desc: 'ECG (requires plugin)',
        highSpeed: false,
        frequencies: [100, 200, 400],
        sensors: [
            { sensorKey: 10, sampleRate: 400 }
        ]
    };

    fixedConfigs[8] = {
        desc: 'Header 3V',
        highSpeed: false,
        frequencies: [100, 200],
        sensors: [
            { sensorKey: 12, sampleRate: 200 },
            { sensorKey: 13, sampleRate: 100 },
            { sensorKey: 21, sampleRate: 100 },
            { sensorKey: 22, sampleRate: 100 },
            { sensorKey: 23, sampleRate: 100 }
        ]
    };

    fixedConfigs[9] = {
        desc: 'Microphone',
        highSpeed: false,
        frequencies: [1200, 2400],
        sensors: [
            { sensorKey: 6, sampleRate: 2400 }
        ]
    };

    fixedConfigs[10] = {
        desc: 'Magnetic',
        highSpeed: false,
        frequencies: [100, 200, 400],
        sensors: [
            { sensorKey: 2, sampleRate: 80 },
            { sensorKey: 12, sampleRate: 400 }
        ]
    };  // 0x0A

    /* ----------- High speed --------- */
    fixedConfigs[32] = { // 0x20
        desc: 'Gyroscope (HS)',
        highSpeed: true,
        frequencies: [800],
        sensors: [
            { sensorKey: 3, sampleRate: 760 }
        ]
    };

    fixedConfigs[12] = {
        desc: 'Header 3V3',
        highSpeed: false,
        frequencies: [100, 200],
        sensors: [
            { sensorKey: 12, sampleRate: 200 },
            { sensorKey: 13, sampleRate: 100 },
            { sensorKey: 21, sampleRate: 100 },
            { sensorKey: 22, sampleRate: 100 },
            { sensorKey: 23, sampleRate: 100 }
        ]
    };   // 0x0C

    fixedConfigs[33] = { // 0x21
        desc: 'Accelerometer (HS)',
        highSpeed: true,
        frequencies: [800],
        sensors: [
            { sensorKey: 1, sampleRate: 800 }
        ]
    };

    fixedConfigs[34] = { // 0x22
        desc: 'Orientation (HS)',
        highSpeed: true,
        frequencies: [100, 200, 400],
        sensors: [
            { sensorKey: 1, sampleRate: 400 },
            { sensorKey: 2, sampleRate: 80 },
            { sensorKey: 3, sampleRate: 190 } // GPI to check !!!
        ]
    };

    fixedConfigs[35] = { // 0x23
        desc: 'Motion',
        highSpeed: true,
        frequencies: [100, 200],
        sensors: [
            { sensorKey: 1, sampleRate: 200 },
            { sensorKey: 3, sampleRate: 190 },
            { sensorKey: 8, sampleRate: 200 },
            { sensorKey: 9, sampleRate: 100 }
        ]
    };

    fixedConfigs[36] = { // 0x24
        desc: 'Sports',
        highSpeed: true,
        frequencies: [100, 200],
        sensors: [
            { sensorKey: 1, sampleRate: 200 },
            { sensorKey: 2, sampleRate: 80 },
            { sensorKey: 3, sampleRate: 190 },
            { sensorKey: 10, sampleRate: 200 }
        ]
    };

    fixedConfigs[37] = { // 0x25
        desc: 'Pendulum (HS)',
        highSpeed: true,
        frequencies: [100, 200],
        sensors: [
            { sensorKey: 1, sampleRate: 200 },
            { sensorKey: 3, sampleRate: 190 },
            { sensorKey: 8, sampleRate: 200 }
        ]
    };

    fixedConfigs[38] = { // 0x26
        desc: 'Kitchen Sink',
        highSpeed: true,
        frequencies: [100],
        sensors: [
            { sensorKey: 1, sampleRate: 100 },
            { sensorKey: 2, sampleRate: 80 },
            { sensorKey: 3, sampleRate: 95 },
            { sensorKey: 4, sampleRate: 100 },
            { sensorKey: 7, sampleRate: 100 },
            { sensorKey: 8, sampleRate: 100 },
            { sensorKey: 9, sampleRate: 100 },
            { sensorKey: 11, sampleRate: 100 },
            { sensorKey: 12, sampleRate: 100 },
            { sensorKey: 13, sampleRate: 100 },
            { sensorKey: 21, sampleRate: 100 },
        ]
    };

    fixedConfigs[39] = { // 0x27
        desc: 'Microphone (HS)',
        highSpeed: true,
        frequencies: [2400, 4800],
        sensors: [
            { sensorKey: 6, sampleRate: 4800 }
        ]
    };

    fixedConfigs[40] = { // 0x28
        desc: 'Ambient Light (HS)',
        highSpeed: true,
        frequencies: [2400, 4800],
        sensors: [
            { sensorKey: 7, sampleRate: 4800 }
        ]
    };

    fixedConfigs[41] = { // 0x29
        desc: 'Ambient Light & Accel (HS)',
        highSpeed: true,
        frequencies: [200, 400, 800],
        sensors: [
            { sensorKey: 1, sampleRate: 800 },
            { sensorKey: 7, sampleRate: 800 }
        ]
    };

    fixedConfigs[42] = { // 0x2A
        desc: 'Force Gauge & Accel (HS)',
        highSpeed: true,
        frequencies: [200, 400, 800],
        sensors: [
            { sensorKey: 1, sampleRate: 800 },
            { sensorKey: 8, sampleRate: 800 }
        ]
    };

    fixedConfigs[43] = { // 0x2B
        desc: 'Ambient Light & Micro (HS)',
        highSpeed: true,
        frequencies: [1200, 2400],
        sensors: [
            { sensorKey: 6, sampleRate: 2400 },
            { sensorKey: 7, sampleRate: 2400 }
        ]
    };

    fixedConfigs[44] = { // 0x2C
        desc: 'Electrocardiograph (HS)',
        highSpeed: true,
        frequencies: [200, 400, 800],
        sensors: [
            { sensorKey: 10, sampleRate: 800 }
        ]
    };

    fixedConfigs[45] = { // 0x2D
        desc: 'High Gain (HS)',
        highSpeed: true,
        frequencies: [2400, 4800],
        sensors: [
            { sensorKey: 12, sampleRate: 4800 }
        ]
    };

    fixedConfigs[46] = { // 0x2E
        desc: 'Force Gauge (HS)',
        highSpeed: true,
        frequencies: [2400, 4800],
        sensors: [
            { sensorKey: 8, sampleRate: 4800 }
        ]
    };

    fixedConfigs[47] = { // 0x2F (47)
        desc: 'ECG & Analog',
        highSpeed: true,
        frequencies: [100, 200, 400],
        sensors: [
            { sensorKey: 241, sampleRate: 400 } // 0xF1
        ]
    };

    /*********************************************************************
     *                  DO NOT MODIFY BELOW THIS LINE                    *
     *********************************************************************/

    function getDongleMode(code) {
        return dongleModes[code];
    }

    function getSensor(code) {
        return sensors[code];
    }

    function getFixedConfig(code) {
        return fixedConfigs[code];
    }

    function getFixedConfigs() {
        return fixedConfigs;
    }

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

    function getAreaUnit(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';
    }

    function getSlopeUnit(unit) {
        if (!unit) {
            return '';
        }
        switch (unit) {
            case 'm/s': return 'm/s²';
            case 'm/s²': return 'm/s³';
            case 'rad/s': return 'rad/s²';
        }
        return unit + '/s';
    }

    /*
     * @param sensorKey, configKey
     * @returns {string}
     */
    function getSampleRate(sensorKey, configKey) {
        var fixedConfig = _.filter(fixedConfigs[configKey].sensors, { sensorKey: sensorKey })[0];
        return fixedConfig.sampleRate;
    }

    /*
     * Sensor array of {code, desc}
     * @returns {Array}
     */
    function getSensors() {
        var res = [];
        _.forEach(sensors, function(sensor) {
            var newEntry = {
                code: sensor.code,
                desc: sensor.desc
            };
            if (sensor.subCharts) {
                newEntry.subCharts = _.map(sensor.subCharts, function(chart, idx) {
                    return { desc: chart.desc, checked: true, coordinate: idx };
                });
            }
            res.push(newEntry);
        });
        return res;
    }

    /*
     * Sensor Config array of {configKey, sensorKey, highSpeed, sampleRate}
     * @returns {Array}
     */
    function getSensorConfigs() {
        var sensorConfigs = [];
        _.forEach(fixedConfigs, function (fixedConfig, configKey) {
            _.forEach(fixedConfig.sensors, function (sensor) {
                sensorConfigs.push({
                    configKey: parseInt(configKey, 10),
                    sensorKey: sensor.sensorKey,
                    highSpeed: fixedConfig.highSpeed,
                    sampleRate: sensor.sampleRate
                });
            });
        });
        return sensorConfigs;
    }

    // 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
    function getTraceSettings(sensorType, coordinate) {
        var traceSettings = [];
        var sensor = sensors[sensorType];

        _.forEach(sensor.legends, function (item, idx) {
            if (coordinate !== undefined && idx !== coordinate) return;
            traceSettings.push({
                show: true,
                legend: item,
                color: sensor.pathColors ? sensor.pathColors[idx] : getDefaultColor(idx)
            });
        });

        return traceSettings;
    }

    return {
        getSensor: getSensor,
        getSensors: getSensors,
        getFixedConfig: getFixedConfig,
        getFixedConfigs: getFixedConfigs,
        getDongleMode: getDongleMode,
        getDefaultColor: getDefaultColor,
        getAreaUnit: getAreaUnit,
        getSlopeUnit: getSlopeUnit,
        getSampleRate: getSampleRate,
        getSensorConfigs: getSensorConfigs,
        getTraceSettings: getTraceSettings
    };
});

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

    var chart = function (_sensor, _acquisitionSet) {

        var remote = _sensor.remote,
            config = _sensor.config,
            sensor = _sensor.sensorType,
            coord = _sensor.coordinate,
            acquisitionSet = _acquisitionSet || IOLab.acquisitionSet(),

            chartID = remote + '-' + config + '-' + sensor + (coord ? ('-' + coord) : ''),

            param = configService.getSensor(sensor) || {},
            subChart = param.subCharts ? param.subCharts[coord] : undefined,
            ecg9 = (sensor === 241);

        // Default width and height (setter/getter)
        var width = 700, height = 400;

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

        // Scale Factors
        var parityFactors = [1];

        // Chart scales' range
        var defScale = { x0: 0, x1: 10 };

        if (param.scales) {
            defScale.y0 = param.scales[0];
            defScale.y1 = param.scales[1];
        } else if (subChart && subChart.scales) {
            defScale.y0 = subChart.scales[0];
            defScale.y1 = subChart.scales[1];
        } else {
            defScale.y0 = -20;
            defScale.y1 = 20;
        }

        var scale = angular.copy(defScale);
        var bbox = {};

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

        // Sampling control variables
        var minScalingRate = param.minScalingRate || 2,
            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 = {};

        // Cursor position
        var cursorPosition = -1;
        var cursorRange = {t0: -1, t1: -1};
        var analyticsData = {};

        /*
         * 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 lastDataSet = [];

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

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

        var resize = {
            active: false,
            d3MouseDown: null,
            d3MouseElement: null
        };
        resize.mousedown = function () {
            resize.active = true;
            resize.d3MouseElement = this;
            resize.d3MouseDown = d3.mouse(this);

            d3.select('body')
                .on('mouseup.resize', resize.mouseup)
                .on('mousemove.resize', resize.mousemove);
        };
        resize.mouseup = function () {
            resize.active = false;
            resize.d3MouseDown = null;

            d3.select('body')
                .on('mouseup.resize', null)
                .on('mousemove.resize', null);
        };
        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;
            if (height < 100 || width < 200) return;
            analyticsData.svgSize = { height: height, width: width};
            dispatch.analytics(analyticsData);
            sampleRateCalc();
            redraw();
        };

        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 || chartMode.zoom_x || chartMode.zoom_y) {
                    redrawZoomSelection();
                }
            }

            // 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 is 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 || chartMode.zoom_x || chartMode.zoom_y) {
                redrawZoomSelection();
            }

            // FFT management
            if (chartMode.stats && fftSamplingRate && !fftLocked) {
                var index = helperService.getDataIndex(lastDataSet, 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 () {

            if (!d3MouseDown) return;

            var mouseDist = Math.sqrt(Math.pow(d3MouseDown[0] - d3.mouse(this)[0], 2) +
                Math.pow(d3MouseDown[1] - d3.mouse(this)[1], 2));

            // Zoom in
            if ((chartMode.zoom || chartMode.zoom_x || chartMode.zoom_y) &&
                selection.notNull() && mouseDist >= 2) {

                // Keep the old scale for further use
                chartMode.scales.push(angular.copy(scale));

                if (chartMode.zoom || chartMode.zoom_x) {
                    scale.x0 = Math.min(selection.p0.x, selection.p1.x);
                    scale.x1 = Math.max(selection.p0.x, selection.p1.x);
                }
                if (chartMode.zoom || chartMode.zoom_y) {
                    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,
                mouseDist = Math.sqrt(Math.pow(d3MouseDown[0] - d3.mouse(this)[0], 2) +
                    Math.pow(d3MouseDown[1] - d3.mouse(this)[1], 2));

            // Zoom out
            if ((chartMode.zoom || chartMode.zoom_x || chartMode.zoom_y) &&
                !!scalesLen && mouseDist < 2) {

                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 || chartMode.zoom_x || chartMode.zoom_y) {
                scale = getMaximunScale();
                chartMode.scales = [];
                sampleRateCalc();
                redraw();
                dispatch.scaleChanges({ x0: scale.x0, x1: scale.x1 });
            }
        };

        //  Determine the largest scale to fully display all acquisitions
        function getMaximunScale (){

            var maxScale = null, tmpScale = null;

            _.forEach(getDataSet(), function(dataSet){

                tmpScale = helperService.computeScale(dataSet.data, defScale, bbox[dataSet.acqID], parityFactors);

                if(!maxScale){
                    maxScale = tmpScale;
                } else {
                    maxScale.x0 = _.min([maxScale.x0, tmpScale.x0]);
                    maxScale.x1 = _.max([maxScale.x1, tmpScale.x1]);
                    maxScale.y0 = _.min([maxScale.y0, tmpScale.y0]);
                    maxScale.y1 = _.max([maxScale.y1, tmpScale.y1]);
                }
            });

            return maxScale;
        }

        var computeEcgSVGMargin = function () {
            // Determine if the trace is the first one or last one
            var count = 0,
                firstEcgTrace = -1,
                lastEcgTrace = -1,
                ecg9Height = window.innerHeight - 310;

            _.forEach(chartTraces, function(item, idx) {
                if (item.show) {
                    if (firstEcgTrace == -1) { firstEcgTrace = idx; }
                    lastEcgTrace = idx;
                    count++;
                }
            });

            margin.top = firstEcgTrace == coord ? 10 : 8;
            margin.bottom = lastEcgTrace == coord ? 35 : 5;
            height = (ecg9Height - 13 * (count - 1)) / count + margin.top + margin.bottom
                + 5 * (9 - count) / count; // Last term is empirical :-/

            if (height == Infinity) height = 100;
        };

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

        var displayStatTime = function() {
            if (!ecg9) return true;

            var firstEcgTrace = -1;
            _.forEach(chartTraces, function(item, idx) {
                if (item.show && firstEcgTrace == -1) {
                    firstEcgTrace = idx;
                }
            });

            return firstEcgTrace == coord;
        };

        var redrawLegend = function () {

            if (cursorPosition == -1) {
                svg.select('.legend-group').style('display', 'none');
                analyticsData.legends = null;
                dispatch.analytics(analyticsData);
                return;
            }

            var timeValue;

            function pushLegends (dataSet) {

                var data = dataSet.data;

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

                timeValue = data[0][index][0];

                // Time value for stat data
                if (displayStatTime() && analyticsData.legends.length === 0) {
                    analyticsData.legends.push({
                        legend: 't', value: timeValue.toFixed(5), unit: 's'
                    })
                }

                // Other values for stat data
                _.forEach(data, function (entry, idx) {
                    if (!showTrace(idx)) { return; }

                    var idx_ = ecg9 ? coord : idx,
                        param_ = subChart || param,
                        parityFactor = parityFactors[idx_ % parityFactors.length];

                    var legend = param_.legends ? (param_.legends[idx_]) : ('x' + idx_),
                        unit = param_.unit || null,
                        value = (entry[index][1] * parityFactor).toFixed(3);

                    analyticsData.legends.push({
                        legend: legend,
                        value: value,
                        unit: unit,
                        color: traceColors(null, idx),
                        opacity: dataSet.opacity
                    });
                });
            }

            // Init legends containers
            analyticsData.legends = [];

            _.forEach(getDataSet(), pushLegends);

            dispatch.analytics(analyticsData);

            if (analyticsData.legends.length === 0) {
                return;
            }

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

            // Update/create a line for the time position
            svg.select('.legend-group')
                .selectAll('.legend-lineX')
                .data([timeValue])  // 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');
                analyticsData.stats = null;
                dispatch.analytics(analyticsData);
                return;
            }

            function pushStatisticsData (dataSet) {
                var data = dataSet.data;
                var subData = [];
                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
                data.forEach(function (entry) {
                    subData.push(entry.slice(index0, index1 + 1));
                });
                var dataSetLen = subData[0].length;

                // Initialization of the object containing the statistics
                var delta_t = Math.abs(subData[0][Math.max(dataSetLen - 1, 0)][0] - subData[0][0][0]);

                if (displayStatTime() && !analyticsData.stats.delta_t) {
                    analyticsData.stats.delta_t = {legend: '∆t', value: delta_t, unit: 's'};

                    analyticsData.stats.t0 = subData[0][0][0];
                    analyticsData.stats.t1 = subData[0][dataSetLen - 1][0];
                }

                // Mean & Standard Deviation calculation (corrected sample standard deviation)
                _.forEach(subData, function (entry, idx) {
                    if (!showTrace(idx)) { return; }

                    var idx_ = ecg9 ? coord : idx,
                        param_ = subChart || param,
                        parityFactor = parityFactors[idx_ % parityFactors.length];

                    var linearReg = helperService.linearRegression(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);

                    var unit = param_.unit || null,
                        areaUnit = configService.getAreaUnit(unit),
                        slopeUnit = configService.getSlopeUnit(unit);

                    analyticsData.stats.data.push({
                        color: traceColors(null, idx),
                        opacity: dataSet.opacity,
                        mean: { legend: 'μ', value: (mean * parityFactor), unit: unit},
                        sigma: { legend: 'σ', value: Math.sqrt(variance),unit: unit },
                        area: { legend: 'a', value: (mean * delta_t * parityFactor), unit: areaUnit},
                        slope: { legend: 's', value: (linearReg.slope * parityFactor), unit: slopeUnit, r2: linearReg.r2}
                    });
                });

                return subData;
            }

            // Time value for stat data
            analyticsData.stats = {
                t0: null,
                t1: null,
                delta_t: null,
                data: []
            };

            _.forEach(getDataSet(), function(dataSet) {
                var data = pushStatisticsData(dataSet);

                // Area under the curve
                svg.select('.statistics-group')
                    .selectAll('.acq-' + dataSet.acqID)
                    .data(data)
                    .attr('d', area)
                    .style('display', function (d, i) {
                        return (!showTrace(i) ? 'none' : null);
                    })
                    .enter().append('path')
                    .attr('class', 'stat-area acq-' + dataSet.acqID )
                    .attr('fill', traceColors)
                    .attr('fill-opacity', 0.25)
                    .attr('clip-path', 'url(#clip-' + chartID + ')');
            });

            dispatch.analytics(analyticsData);

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

            // xScale on the start and end time of the data set
            var svgData = [[xScale(analyticsData.stats.t0), xScale(analyticsData.stats.t1)]];

            // 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.15)
                .attr('clip-path', 'url(#clip-' + chartID + ')');
        };

        var redrawZoomSelection = function () {

            /*
             * Cross
             */
            var cross_x = [],
                cross_y = [],
                crossGroup;

            analyticsData.legends = null;

            if ((chartMode.zoom || chartMode.zoom_x) && d3Mouse && !selection.p0.locked) {
                cross_x = [d3Mouse[0]];
            }
            if ((chartMode.zoom || chartMode.zoom_y) && d3Mouse && !selection.p0.locked) {
                cross_y = [d3Mouse[1]];
            }

            if ((chartMode.zoom || chartMode.zoom_x || chartMode.zoom_y) && d3Mouse) {
                var x = xScale.invert(d3Mouse[0]);
                var y = yScale.invert(d3Mouse[1]);

                analyticsData.legends = [{
                    legend: 't', value: x.toFixed(4), unit: 's'
                }];

                var unit = (subChart || param).unit || '';

                analyticsData.legends.push({
                    legend: 'x', value: y.toFixed(3), unit: unit
                });
            }

            dispatch.analytics(analyticsData);

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

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


            /*
             * Rectangle
             */
            var zoomData, zoomGroup;

            if (selection.p0.x !== -1 && selection.p1.x !== -1) {
                zoomData = [{}];
                if (chartMode.zoom_y) {
                    zoomData[0].x0 = scale.x0;
                    zoomData[0].x1 = scale.x1;
                } else {
                    zoomData[0].x0 = selection.p0.x < selection.p1.x ? selection.p0.x : selection.p1.x;
                    zoomData[0].x1 = selection.p0.x < selection.p1.x ? selection.p1.x : selection.p0.x;
                }
                if (chartMode.zoom_x) {
                    zoomData[0].y0 = scale.y0;
                    zoomData[0].y1 = scale.y1;
                } else {
                    zoomData[0].y0 = selection.p0.y < selection.p1.y ? selection.p0.y : selection.p1.y;
                    zoomData[0].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 traceColors = function (d, i) {
            if (subChart && subChart.pathColors) {
                return subChart.pathColors[i];
            }
            if (param.pathColors) {
                if (ecg9) {
                    return param.pathColors[coord];
                }
                return param.pathColors[i];
            }
            return configService.getDefaultColor(i);
        };

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

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

        var smoothingPath = function (nbrOfPoints) {

            var _sidePoints = (nbrOfPoints * 1 - 1) / 2;

            if (getDataSet().length == 0 || _sidePoints === null) {
                return;
            }

            _.forEach(getDataSet(), function(dataSet) {

                if (!dataSet.data.length) {
                    return;
                }

                if (sidePoints[dataSet.acqID] === _sidePoints) {
                    return;
                }

                sidePoints[dataSet.acqID] = _sidePoints;

                if (_sidePoints === 0) {
                    helperService.resetData(dataSet.data);
                } else {
                    helperService.smoothData(dataSet.data, _sidePoints);
                }
            });
        };

        // FIXME - usage of data
        var computeFftSpectrum = function (index) {

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

        // FIXME - usage of data
        var redrawFftSelection = function () {
            var fftSelection = [];

            if (fftResult !== null) {
                fftSelection = [[
                    xScale(lastDataSet[0][fftResult.index - fftSamplingRate / 2][0]),
                    xScale(lastDataSet[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)
                .attr('fill', samplingRate > minScalingRate ? traceColors : 'none');

            // Change decrease opacity
            var dataSets = getDataSet();

            _.forEach(dataSets, function(dataSet) {
                if (!dataSet.acqID) { return; }

                svg.select('.chart-group')
                    .selectAll('.acq-' + dataSet.acqID)
                    .attr('opacity', dataSet.opacity);
            });
        };

        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);
            yAxis.ticks(height < 150 ? 5 : 10);

            svg.attr({ width: width, height: height });
            svg.attr({ viewBox: "0 0 " + width + " " + height });
            svg.attr({ preserveAspectRatio: "xMidYMid meet" });

            svg.select('g.container-group')
                .attr({ transform: 'translate(' + margin.left + ',' + margin.top + ')' });

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

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

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

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

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


        // Return the decorated dataSet (acqID if available)
        var getDataSet = function() {

            var dataSet = [],
                runsLength = acquisitionSet.runs.length;

            // forEach since we can image to need to filter entries ...
            _.forEach(acquisitionSet.runs, function(run, idx) {
                var data = svg.select('.chart-group')
                    .selectAll('.acq-' + run.acqID)
                    .data();

                if (!data || data.length == 0) { return; }

                dataSet.push({
                    acqID: run.acqID,
                    name: run.name,
                    data: data,
                    opacity: 0.9 - (runsLength < 2 ? 0 : (runsLength - 1 - idx) * 0.4 / (runsLength - 1))
                });

            });

            return dataSet;
    };

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

            if (!svg) {

                svg = divContainer.select('.chart-holder')
                    .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('Time (s)')
                    .attr({'text-anchor': 'end', 'alignment-baseline': 'middle'})
                    .style({'font-size': 12});

                container.append('g').classed('y-axis-group axis', true)
                    .append('g').attr('class', 'y-axis-legend')
                    .append('text').text(((subChart || param).label || '') + ' (' + ((subChart || param).unit || ['n.a.']) + ')')
                    .attr({'text-anchor': 'middle', 'alignment-baseline': 'middle'})
                    .style({'font-size': 13});

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

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


                var d1 = function(d) { return d[1];};

                // Return the y-value in px (used with line & area functions)
                var d3yScale = function(_data, _idx) {
                    var factor = parityFactors[_idx % parityFactors.length];
                    return function (d, i) {
                        if (samplingRate == 1) {
                            return yScale(d[1] * factor);
                        }
                        return yScale(d3.mean(_data.slice(i, i + samplingRate), d1) * factor);
                    };
                };

                // Return the y-value max in px (used with line & area functions)
                var d3yScaleMax = function(_data, _idx) {
                    var factor = parityFactors[_idx % parityFactors.length];
                    return function (d, i) {
                        return yScale(d3.max(_data.slice(i, i + samplingRate), d1) * factor);
                    };
                };

                // Return the y-value min in px (used with line & area functions)
                var d3yScaleMin = function(_data, _idx) {
                    var factor = parityFactors[_idx % parityFactors.length];
                    return function (d, i) {
                        return yScale(d3.min(_data.slice(i, i + samplingRate), d1) * factor);
                    };
                };

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

                line = function (_data, _idx) {
                    var d3line;
                    if (samplingRate > minScalingRate) {
                        d3line = d3.svg.area()
                            .interpolate('linear')
                            .x(function (d) { return xScale(d[0]); }) //  - 0.005 for 100Hz
                            .y0(d3yScaleMin(_data, _idx))
                            .y1(d3yScaleMax(_data, _idx))
                            .defined(d3defined);
                    } else {
                        d3line = d3.svg.line()
                            .interpolate('linear')  // step-before is the correct one ...
                            .x(function (d) { return xScale(d[0]); }) //  - 0.005 for 100Hz
                            .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]); }) //  - 0.005 for 100Hz
                        .y0(function () { return yScale(0); })
                        .y1(d3yScale(_data, _idx))
                        .defined(d3defined);
                    return d3area(_data);
                };
            }

            redraw();
            redrawLegend();
            showHideTraces();
        }

        // FIXME - usage of data
        exports.reset = function () {
            // Reset cursorRange
            dispatch.cursorRange({ t0: -1, t1: -1});

            // Check if blob loaded
            if (!lastDataSet || lastDataSet.length == 0) { return; }
            // Reset the zoom scale to the bbox
            scale = getMaximunScale();
            chartMode.scales = [];
            sampleRateCalc();
            redraw();
            dispatch.scaleChanges({ x0: scale.x0, x1: scale.x1 });
        };

        // FIXME - usage of data
        // Add or update '.path'
        exports.addData = function (_data, _opts) {

            var _class = _opts.cl,
                _overlap = _opts.overlap,
                _running = _opts.running,
                _computeScale = _opts.computeScale;

            // New data on existing data
            if (_data.length && lastDataSet.length) {

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

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

                // Time of the first new data point to add the data array ...
                var startTime = Math.max(lastDataSet[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
                lastDataSet.forEach(function (entry, i) {
                    for (var n = index; n < newDataLength; n++) {
                        entry.push(_data[i][n]);
                    }
                });
            }

            // New data on empty chart
            if (_data.length && !lastDataSet.length) {
                lastDataSet = _data;

                // Keep a copy of the original value...
                _data.forEach(function (trace) {
                    trace.forEach(function (entry) {
                        entry[2] = entry[1];
                    });
                });
            }

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

            if (_computeScale) {
                if ((subChart || param).autoScaleY) {
                    scale = getMaximunScale();
                } else {
                    scale = helperService.computeScale(_data, defScale);
                }
            }
        };

        // Remove all '.paths'
        exports.removeData = function (_class) {
            lastDataSet = [];

            svg.select('.chart-group')
                .selectAll(_class || '.paths')
                .remove();

            svg.select('.statistics-group')
                .selectAll(_class || '.paths')
                .remove();

            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.newRun = function () {
            lastDataSet = [];
            redrawPaths();
        };

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

            if (ecg9) {
                computeEcgSVGMargin();
            }

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

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

            if (yAxis) {
                yAxis.ticks(height < 150 ? 5 : 10);
                svg.select('.y-axis-group.axis').call(yAxis);
            }

            return this;
        };

        exports.bbox = function (_x, acqID) {

            if (!arguments.length) {
                return bbox;
            }

            bbox[acqID] = _x;

            return this;
        };

        exports.showTraces = function (_x) {
            if (!arguments.length) {
                return chartTraces;
            }
            chartTraces = _x;
            if (ecg9) {
                computeEcgSVGMargin();
                redraw();
            } else {
                redrawLegend();
                redrawStatistics();
                showHideTraces();
            }
            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) {
            var deltaScaleX = defScale.x1 - defScale.x0;

            scale.x0 = time > deltaScaleX ? time : 0;
            scale.x1 = scale.x0 + defScale.x1;
            scale.y0 = defScale.y0;
            scale.y1 = defScale.y1;
            return this;
        };

        exports.scale = function (_x, opt) {
            if (!arguments.length) {
                return scale;
            }
            if (opt && opt.getDefScale) {
                return defScale;
            }

            var deltaX = scale.x1 - scale.x0;

            scale.x0 = _x.x0 != undefined ? _x.x0 : scale.x0;
            scale.x1 = _x.x1 != undefined ? _x.x1 : scale.x1;
            scale.y0 = _x.y0 != undefined ? _x.y0 : scale.y0;
            scale.y1 = _x.y1 != undefined ? _x.y1 : scale.y1;

            if (opt && opt.setDefScale) {
                defScale.x0 = _x.x0 != undefined ? _x.x0 : defScale.x0;
                defScale.x1 = _x.x1 != undefined ? _x.x1 : defScale.x1;
                defScale.y0 = _x.y0 != undefined ? _x.y0 : defScale.y0;
                defScale.y1 = _x.y1 != undefined ? _x.y1 : defScale.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(_x);
            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.parityFactors = function(_x) {
            if (!arguments.length) {
                return parityFactors;
            }
            parityFactors = _x;
            redrawPaths();
            redrawStatistics();
            return this;
        };

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

        return exports;
    };

    return chart;
}]);

/**
 * Created by geoffroy on 29/10/15.
 */

angular.module('IOLabApp').directive('iolabAnalytics', function () {
    'use strict';
    return {
        restrict: 'E',
        replace: true,
        scope: true,
        require: ['?^iolabChart', '?^iolabEcgSvg'],
        templateUrl: 'shared/chart.analytics.tpl.html',
        link: function (scope, element, attrs, ctrls) {

            _.forEach(ctrls, function(ctrl) {
                if (!ctrl) return;

                // Register fft.redraw callback to update data when they are available
                // in iolab-chart directive
                ctrl.setAnalyticsData = function (data) {
                    scope.data = data;
                    scope.$$phase || scope.$apply();
                };

                scope.$on('$destroy', function () {
                    ctrl.setAnalyticsData = null;
                });
            });
        }
    };
});