if (!Array.prototype.front) {
    Array.prototype.front = function () {
        return this[0];
    };
};

if (!Array.prototype.back) {
    Array.prototype.back = function () {
        return this[this.length - 1];
    };
};

angular.module('quasardbGUIApp.services', [])

    // quasardb querying service
    .factory('quasardbAPIService', ['$http', function ($http) {
        var quasardbAPI = {};
        var regexpDateHistory = /^[0-9]{4}[0-1]{1}[0-9]{1}[0-3]{1}[0-9]{1}[0-2]{1}[0-9]{1}[0-5]{1}[0-9]{1}$/;

        // ** Cluster services : **
        //     -> Ask quasardb for cluster topology
        quasardbAPI.getClusterTopology = function (id, mock) {
            if (mock) {
                return $http({
                    method: 'GET',
                    url: 'mocks/cluster_topology.json'
                });
            } else {
                return $http({
                    method: (id ? 'JSONP' : 'GET'),
                    headers: {},
                    url: '../cluster_topology' + (id ? '?daemon=' + id + '&callback=JSON_CALLBACK' : '')
                });
            }
        };

        //     -> Ask quasardb for latest cluster data
        quasardbAPI.getClusterData = function (id, mock) {
            if (mock) {
                return $http({
                    method: 'GET',
                    url: 'mocks/cluster_status.json'
                });
            } else {
                return $http({
                    method: (id ? 'JSONP' : 'GET'),
                    headers: {},
                    url: '../cluster_status' + (id ? '?daemon=' + id + '&callback=JSON_CALLBACK' : '')
                });
            }
        };

        //     -> Ask quasardb for cluster history
        quasardbAPI.getClusterHistoricalData = function (id, mock, start, end) {
            if (mock) {
                return $http({
                    method: 'GET',
                    url: 'mocks/cluster_history.json'
                });
            } else {
                // Provide yesterday date
                var today = new Date();
                var yesterday = new Date();
                yesterday.setTime(today.getTime() - 24 * 60 * 60 * 1000);
                quasardbAPI.yesterday = yesterday;

                // Launch request
                return $http({
                    method: (id ? 'JSONP' : 'GET'),
                    headers: {},
                    url: '../cluster_history' + (id ? '?daemon=' + id
                        + '&start=' + (regexpDateHistory.test(start) ? start : yesterday.format("yyyymmddHHMM"))
                        + '&end=' + (regexpDateHistory.test(end) ? end : today.format("yyyymmddHHMM"))
                        + '&callback=JSON_CALLBACK' : '')
                });
            }
        };

        // ** Node services : **
        //     -> Ask quasardb for data of one node
        quasardbAPI.getNodeData = function (id, mock) {
            if (mock) {
                return $http({
                    method: 'GET',
                    url: 'mocks/' + (id ? id : 'node1') + '_data.json'
                });
            } else {
                return eval($http({
                    method: (id ? 'JSONP' : 'JSON'),
                    url: '../global_status' + (id ? '?daemon=' + id + '&callback=JSON_CALLBACK' : '')
                }));
            }
        };

        //     -> Ask quasardb for one node's configuration
        quasardbAPI.getNodeConfiguration = function (id, mock) {
            if (mock) {
                return $http({
                    method: 'GET',
                    url: 'mocks/' + (id ? id : 'node1') + '_conf.json'
                });
            } else {
                return $http({
                    method: (id ? 'JSONP' : 'JSON'),
                    url: '../config' + (id ? '?daemon=' + id + '&callback=JSON_CALLBACK' : '')
                });
            }
        };

        //     -> Ask quasardb for data of one node
        quasardbAPI.getNodeHistoricalData = function (id, mock, start, end) {
            if (mock) {
                return $http({
                    method: 'GET',
                    url: 'mocks/' + (id ? id : 'node1') + '_history.json'
                });
            } else {
                // Provide yesterday date
                var today = new Date();
                var yesterday = new Date();
                var one_day_in_seconds = 24 * 60 * 60;
                var one_day_in_milliseconds = one_day_in_seconds * 1000;
                yesterday.setTime(today.getTime() - one_day_in_milliseconds);
                quasardbAPI.yesterday = yesterday;

                // Launch request
                return $http({
                    method: (id ? 'JSONP' : 'JSON'),
                    url: '../history' + (
                        id ?
                            '?daemon=' + id
                            + '&start=' + (regexpDateHistory.test(start) ? start : yesterday.format("yyyymmddHHMM"))
                            + '&end=' + (regexpDateHistory.test(end) ? end : today.format("yyyymmddHHMM"))
                            + '&callback=JSON_CALLBACK'
                            : '')
                });
            }
        };

        return quasardbAPI;
    }])

    .factory('dataFunctions', ['$log', '$filter', '$route', '$rootScope', 'quasardbSettings', function ($log, $filter, $route, $rootScope, quasardbSettings) {
        var dataFunctions = {};

        // Remove duplicate from an array
        dataFunctions.removeDuplicates = function (a) {
            var seen = {};
            var out = [];
            var len = a.length;
            var j = 0;
            for (var i = 0; i < len; i++) {
                var item = a[i];
                if (seen[item] !== 1) {
                    seen[item] = 1;
                    out[j++] = item;
                }
            }
            return out;
        };

        // Return a compliant JSON from cluster or node data
        dataFunctions.buildValues = function (infos, values) {
            $log.debug('dataFunctions.buildValues(infos, values) -> ', infos, values, $rootScope);
            if (values === null) {
                return {};
            }

            var r_max_count = 0;
            var r_max_bytes = 0;
            var p_max_bytes = 0;

            // Check data case :
            if (infos instanceof Array) {
                //     => Historical : get the last point
                lastInfo = infos.back()
                r_max_count = lastInfo.value.r_max_count;
                r_max_bytes = lastInfo.value.r_max_bytes;
                p_max_bytes = lastInfo.value.p_max_bytes;
            } else if (infos.values) {
                //     => Cluster's case : aggregate each max values
                angular.forEach(infos.values, function (value) {
                    r_max_count += value.r_max_count;
                    r_max_bytes += value.r_max_bytes;
                    p_max_bytes += value.p_max_bytes;
                });
            } else {
                //     => Node's case : read max values
                r_max_count = infos.value.r_max_count;
                r_max_bytes = infos.value.r_max_bytes;
                p_max_bytes = infos.value.p_max_bytes;
            }

            var currentNode = $rootScope.nodelist[$rootScope.indexCurrentNode]

            // Format output :
            return {
                //     -> Common part :
                "date": d3.time.format.iso.parse(values.timestamp),
                "status": (currentNode === undefined) ? 'online' : currentNode.status,
                "put": values.operations.put.count,
                "put_failed": values.operations.put.failures,
                "get": values.operations.get.count,
                "get_failed": values.operations.get.failures,
                "update": values.operations.update.count,
                "update_failed": values.operations.update.failures,
                "get_and_update": values.operations.get_and_update.count,
                "get_and_update_failed": values.operations.get_and_update.failures,
                "remove": values.operations.remove.count,
                "remove_failed": values.operations.remove.failures,
                "remove_if": values.operations.remove_if.count,
                "remove_if_failed": values.operations.remove_if.failures,
                "get_and_remove": values.operations.get_and_remove.count,
                "get_and_remove_failed": values.operations.get_and_remove.failures,
                "cas": values.operations.compare_and_swap.count,
                "cas_failed": values.operations.compare_and_swap.failures,
                "vm": values.memory.vm.used,
                "mem": values.memory.physmem.used,
                "disk": values.disk_usage.total,
                "free": values.disk_usage.free,
                "evictions": values.overall.evictions,
                "entries": values.overall.count,
                "p_size": values.entries.persisted.size,
                "p_count": values.entries.persisted.count,
                "r_size": values.entries.resident.size,
                "r_count": values.entries.resident.count,
                "cpu_average": values.cpu,
                "cpu_idle": values.cpu_idle,
                "ram": values.memory.physmem.total,
                "r_max_count": r_max_count,
                "r_max_bytes": r_max_bytes,
                "p_max_bytes": p_max_bytes,
                "max_size": values.disk_usage.total,

                //     -> Node part :
                "node_id": values.node_id,
                "node_hostname": (currentNode === undefined) ? 'unknown' : currentNode.hostname,
                "node_ip": (values.network !== undefined) ? values.network.listening_endpoint : '',
                "os": values.operating_system,
                "build": values.engine_build_date,
                "version": values.engine_version,
                "nbCpu": values.hardware_concurrency,
                "partitions": (values.network !== undefined) ? values.network.partitions : '',
                "uptime": Date.DateDiff(new Date(values.startup), new Date(/*values.timestamp*/)),
                "transient_mode": (currentNode === undefined) ? false : currentNode.transient_mode
            };
        };

        // Build history from provided values
        dataFunctions.buildHistory = function (scope, clusterInfo, values) {
            if (values.count > 0) {
                $log.debug('dataFunctions.buildHistory(scope, clusterInfo, values) - ', scope, clusterInfo, values, scope.$root);

                // Compute new data :
                var firstCPU = 0;
                var secondCPU = 0;
                var nbCpu = 0;
                var timestamp = null;

                if (scope.tab == 2 && scope.$parent.nodeData === undefined) {
                    scope.$parent.nodeData = [];
                }

                // Browse history samples :
                angular.forEach(values.results, function (result) {
                    //  -> Compute cluster CPU usage
                    secondCPU = firstCPU;
                    firstCPU = result.cpu_times.idle;
                    var cpu = (timestamp == null || nbCpu !== result.hardware_concurrency) ? 0 : (100 - (Math.max(firstCPU - secondCPU, 0) / result.hardware_concurrency / (new Date(result.timestamp).getTime() - new Date(timestamp).getTime()) / 10));

                    //  -> Send computed data
                    timestamp = result.timestamp;
                    nbCpu = result.hardware_concurrency;
                    result.cpu = Math.round(cpu * 100) / 100;
                    result.cpu_idle = firstCPU;

                    //   -> Fetch cluster data
                    if (scope.tab == 1) {
                        scope.$parent.clusterData.push({
                            key: result.timestamp,
                            value: dataFunctions.buildValues(clusterInfo, result)
                        });
                    } else {
                        //     -> Fetch node data
                        scope.$parent.nodeData.push({
                            key: result.timestamp,
                            value: dataFunctions.buildValues(scope.$parent.clusterData, result)
                        });
                    }
                });
            }
        };

        // Add computed data to graphs
        dataFunctions.setGraphData = function ($scope, data) {
            $log.debug("dataFunctions.setGraphData($scope, data) => ", $scope, data);

            // No data => return
            if ((data === undefined) || (data.length == 0)) {
                return;
            }

            // Update scope
            $scope.charts = quasardbSettings.charts;
            $scope.ranges = quasardbSettings.ranges;

            var firstValue = data.front().value;
            // Compute limiters :
            var p_max_count = (firstValue.r_max_count ? firstValue.r_max_count : quasardbSettings.max_count);
            var r_max_count = p_max_count;
            var p_max_size = (firstValue.p_max_bytes ? firstValue.p_max_bytes : 0);
            //   -> max_size == 0 means no limit setted up => max_size = max disk space
            if (p_max_size == 0) {
                p_max_size = firstValue.max_size;
            }
            var r_max_size = (firstValue.r_max_bytes ? firstValue.r_max_bytes : 0);
            $log.debug("dataFunctions.setGraphData() -> maxs are ", p_max_count, r_max_count, p_max_size, r_max_size, ' - initial data is ', data.front());

            // Update scope in order to refresh graphs
            // TODO : fill preview events
            angular.forEach($scope.charts, function (chart, idxChart) {
                var newMax = [];
                angular.forEach(chart.max, function (value) {
                    if (eval(value) !== undefined) {
                        newMax.push(eval(value));
                    } else {
                        newMax.push(value);
                    }
                });
                $scope.charts[idxChart].max = newMax;

                var newData = [];
                angular.forEach(chart.data, function (value) {
                    var toEval = 'var ' + value + '= [];' +
                        'angular.forEach(data, function(aData, idx) { ' +
                        '   var timestamp = d3.time.format.iso.parse(aData.key).getTime() / 1000;' +
                        '   angular.forEach(aData.value, function(value, name) {' +
                        '      if (name == "' + value + '") { ' +
                        value + '.push({' +
                        '              x: timestamp, ' +
                        '              y: value' +
                        '         });' +
                        '      }' +
                        '    });' +
                        '}); ' +
                        'if (' + value + '.length > quasardbSettings.maxPoints) ' + value + '.shift();' +
                        '$scope.' + value + ' = ' + value + ';';
                    $log.debug('dataFunctions.setGraphData() toEval => ', toEval, ' - Values => ', data);
                    eval(toEval);
                    newData.push(value);
                });
                $scope.charts[idxChart].data = newData;

                $log.debug('dataFunctions.setGraphData() ', chart.data, ' *** ', $scope);
            });
        };

        // Update graph data and preview graphs
        dataFunctions.updateData = function (scope, graph, spinner, computedData) {
            $log.debug('dataFunctions.updateData(scope, graph, spinner, computedData) => ', scope, graph, spinner, computedData);
            var newData = [[], []];

            // Don't refresh already loaded period
            if (scope.histoLoaded !== undefined) {
                scope.histoLoaded[scope.selectedRange] = true;
            }

            // Compute all new data
            angular.forEach(computedData, function (data) {
                $log.debug('dataFunctions.updateData => ', data);
                newData[0].push({
                    x: d3.time.format.iso.parse(data.key).getTime() / 1000,
                    y: eval('data.value.' + scope.data[0])
                });
                if (scope.data.length > 1) {
                    newData[1].push({
                        x: d3.time.format.iso.parse(data.key).getTime() / 1000,
                        y: eval('data.value.' + scope.data[1])
                    });
                }
            });
            $log.debug('dataFunctions.updateData => newData = ', newData);

            // Refresh graphs and preview graphs
            if (graph !== null) {
                angular.forEach(graph.series, function (serie, idx) {
                    serie.data = newData[idx].sort(function (a, b) {
                        var c = new Date(a.x);
                        var d = new Date(b.x);
                        return c - d;
                    });
                });
                graph.window.xMin = graph.dataDomain()[0];
                graph.window.xMax = graph.dataDomain()[1];

                $log.debug('dataFunctions.updateData(scope, graph, spinner, computedData) => ', scope, graph, computedData, ' - domains => ', graph.dataDomain(), graph.window.xMin, graph.window.xMax);

                if (spinner !== null) {
                    spinner.stop();
                }
                graph.update();
            }
        };

        return dataFunctions;
    }])

    .factory('shortcuts', ['$log', '$route', '$rootScope', 'quasardbSettings', 'quasardbAPIService', 'dataFunctions', function ($log, $route, $rootScope, quasardbSettings, quasardbAPIService, dataFunctions) {
        var nodesLoaded = new HashMap();
        return {
            // Reset nodes list
            resetNodesLoaded: function () {
                nodesLoaded.clear();
            },

            // function dynamically called when browsing specific historical periods
            //     => See app.js for configuration
            setRange: function (scope, graph, period) {
                var activeTab = scope.$parent.tab;
                $log.debug('shortcuts.setRange(scope, graph, period) => ', scope, graph, period, ' - Tab => ', activeTab);
                if (graph !== null) {
                    var startDate = new Date();
                    if (period === "h") {
                        startDate.setHours(startDate.getHours() - 4);
                    } else {
                        startDate.setDate(startDate.getDate() - 2);
                    }
                    var data = (activeTab == 1) ? scope.$parent.clusterData : scope.$parent.nodeData;
                    data = data.filter(function (element) {
                        return (new Date(element.key)).getTime() > startDate.getTime();
                    });
                    $log.debug('shortcuts.setRange(scope, graph, period) => ', (activeTab == 1) ? scope.$parent.clusterData : [], ' - filtered data : ', data);
                    dataFunctions.updateData(scope, graph, null, data);
                }
            },

            // function dynamically called when browsing specific historical periods
            //     => See app.js for configuration
            getHistoric: function (scope, graph, start, end) {
                $log.debug('shortcuts Service : getHistoric(scope, start, end) => ', scope, start, end);
                var activeTab = scope.$parent.tab;
                var spinner = new Spinner(quasardbSettings.loaderOptions).spin(graph.element);

                // Check if data aren't already loaded
                if (scope.$root.clusterHistoricLoaded === undefined) {
                    scope.$root.clusterHistoricLoaded = [];
                    angular.forEach(scope.ranges, function () {
                        scope.$root.clusterHistoricLoaded.push(false);
                    });
                }
                $log.debug('shortcuts Service : getHistoric() : histoLoaded => ', scope.$root.clusterHistoricLoaded, nodesLoaded.get(scope.$root.nodelist[scope.$root.indexCurrentNode].ip));

                // Don't get already loaded period
                if (((scope.$root.clusterHistoricLoaded[scope.selectedRange] === true) && (activeTab == 1)) || (nodesLoaded.get(scope.$root.nodelist[scope.$root.indexCurrentNode].ip) !== undefined)) {
                    var data = (activeTab == 1) ? scope.$parent.clusterData : scope.$parent.nodeData;
                    data = data.filter(function (element) {
                        return (new Date(element.key)).getTime() > (new Date(("" + start).substring(0, 4), ("" + start).substring(4, 6) - 1, ("" + start).substring(6, 8), ("" + start).substring(8, 10), ("" + start).substring(10, 12), 59)).getTime();
                    });
                    $log.debug('shortcuts.getHistoric(scope, start, end) ', scope, start, end, ' => ', data);
                    dataFunctions.updateData(scope, graph, spinner, data);
                } else {
                    var computedEnd = quasardbAPIService.yesterday.format("yyyymmddHHMM");
                    ((activeTab == 1) ? quasardbAPIService.getClusterHistoricalData(scope.$root.nodelist.front().ip, $route.current.params.mock, start, computedEnd) : quasardbAPIService.getNodeHistoricalData(scope.$root.nodelist[scope.$root.indexCurrentNode].ip, $route.current.params.mock, start, computedEnd)).
                        //  -> cluster historic retrieved
                        success(function (response) {
                            dataFunctions.buildHistory(scope.$parent, (activeTab == 1) ? scope.$parent.clusterData : scope.$parent.nodeData, response);
                            var data = (activeTab == 1) ? scope.$parent.clusterData : scope.$parent.nodeData;
                            dataFunctions.updateData(scope, graph, spinner, data);
                            (activeTab == 1) ? scope.$root.clusterHistoricLoaded[scope.selectedRange] = true : nodesLoaded.add(scope.$root.nodelist[scope.$root.indexCurrentNode].ip, 'OK');
                        }).

                        //  -> cluster historic KO
                        error(function (data, status, headers, config) {
                            $log.info('clusterViewController : getCluster(id, mock) - getClusterHistoricalData(node, mock, start, end) ', (activeTab == 1) ? scope.$root.nodelist.front().ip : scope.$root.nodelist[scope.$root.indexCurrentNode].ip, $route.current.params.mock, 'null, null => ERROR - ', data, status, headers, config);
                            if (spinner !== null) {
                                spinner.stop();
                            }
                        }
                        );
                }
            }
        };
    }]);
