/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

(function() {
    var angular = require('angular');
    var d3 = require('d3');

    require('./kube-client');
    require('./kube-client-cockpit');
    require('./utils');

    angular.module('kubernetes.graph', [
        'kubeClient',
        'kubeClient.cockpit',
        'kubeUtils',
    ])

            .factory('CAdvisorSeries', [
                "KubeRequest",
                "CockpitMetrics",
                "$exceptionHandler",
                "$timeout",
                function (KubeRequest, CockpitMetrics, $exceptionHandler, $timeout) {
                    function CAdvisor(node) {
                        var self = this;

                        /* called when containers changed */
                        var callbacks = [];

                        /* cAdvisor has 10 second intervals */
                        var interval = 10000;

                        var last = { };

                        var requests = { };

                        /* Holds the container specs */
                        self.specs = { };

                        function feed(containers) {
                            var x, y, ylen, i, len;
                            var item, offset, timestamp, container, stat;

                            /*
                     * The cAdvisor data doesn't seem to have inherent guarantees of
                     * continuity or regularity. In theory each stats object can have
                     * it's own arbitrary timestamp ... although in practice they do
                     * generally follow the interval to within a few milliseconds.
                     *
                     * So we first look for the lowest timestamp, treat that as our
                     * base index, and then batch the data based on that.
                     */

                            var first = null;

                            for (x in containers) {
                                container = containers[x];
                                if (container.stats) {
                                    len = container.stats.length;
                                    for (i = 0; i < len; i++) {
                                        timestamp = container.stats[i].timestamp;
                                        if (timestamp) {
                                            if (first === null || timestamp < first)
                                                first = timestamp;
                                        }
                                    }
                                }
                            }

                            if (first === null)
                                return;

                            var base = Math.floor(new Date(first).getTime() / interval);

                            var items = [];
                            var name;
                            var mapping = { };
                            var new_ids = [];
                            var id;
                            var names = { };

                            for (x in containers) {
                                container = containers[x];

                                /*
                         * This builds the correct type of object graph for the
                         * paths seen in grid.add() to operate on
                         */
                                name = container.name;
                                if (!name)
                                    continue;

                                names[name] = name;
                                mapping[name] = { "": name };
                                id = name;

                                if (container.aliases) {
                                    ylen = container.aliases.length;
                                    for (y = 0; y < ylen; y++) {
                                        mapping[container.aliases[y]] = { "": name };

                                        /* Try to use the real docker container id as our id */
                                        if (container.aliases[y].length === 64)
                                            id = container.aliases[y];
                                    }
                                }

                                if (id && container.spec) {
                                    if (!self.specs[id]) {
                                        self.specs[id] = container.spec;
                                        new_ids.push(id);
                                    }
                                }

                                if (container.stats) {
                                    len = container.stats.length;
                                    for (i = 0; i < len; i++) {
                                        stat = container.stats[i];
                                        if (!stat.timestamp)
                                            continue;

                                        /* Convert the timestamp into an index */
                                        offset = Math.floor(new Date(stat.timestamp).getTime() / interval);

                                        item = items[offset - base];
                                        if (!item)
                                            item = items[offset - base] = { };
                                        item[name] = stat;
                                    }
                                }
                            }

                            if (new_ids.length > 0)
                                invokeCallbacks(new_ids);

                            /* Make sure each offset has something */
                            len = items.length;
                            for (i = 0; i < len; i++) {
                                if (items[i] === undefined)
                                    items[i] = { };
                            }

                            /* Now for each offset, if it's a duplicate, put in a copy */
                            for (name in names) {
                                len = items.length;
                                for (i = 0; i < len; i++) {
                                    if (items[i][name] === undefined)
                                        items[i][name] = last[name];
                                    else
                                        last[name] = items[i][name];
                                }
                            }

                            self.series.input(base, items, mapping);
                        }

                        function request(query) {
                            var body = JSON.stringify(query);

                            /* Only one request active at a time for any given body */
                            if (body in requests)
                                return;

                            var path = "/api/v1/proxy/nodes/" + encodeURIComponent(node) + ":4194/api/v1.2/docker";
                            var req = KubeRequest("POST", path, query);

                            requests[body] = req;
                            req.then(function(data) {
                                delete requests[body];
                                feed(data.data);
                            })
                                    .catch(function(ex) {
                                        delete requests[body];
                                        if (ex.status != 503)
                                            console.warn(ex);
                                    });
                        }

                        function invokeCallbacks(/* ... */) {
                            var i, len, func;
                            for (i = 0, len = callbacks.length; i < len; i++) {
                                func = callbacks[i];
                                try {
                                    if (func)
                                        func.apply(self, arguments);
                                } catch (e) {
                                    $exceptionHandler(e);
                                }
                            }
                        }

                        self.fetch = function fetch(beg, end) {
                            var query;
                            if (!beg || !end) {
                                query = { num_stats: 60 };
                            } else {
                                query = {
                                    start: new Date((beg - 1) * interval).toISOString(),
                                    end: new Date(end * interval).toISOString()
                                };
                            }
                            request(query);
                        };

                        self.close = function close() {
                            var req, body;
                            for (body in requests) {
                                req = requests[body];
                                if (req && req.cancel)
                                    req.cancel();
                            }
                        };

                        self.watch = function watch(callback) {
                            var ids;
                            var timeout;
                            callbacks.push(callback);
                            if (self.specs) {
                                ids = Object.keys(self.specs);
                                if (ids.length > 0) {
                                    timeout = $timeout(function() {
                                        timeout = null;
                                        callback.call(self, ids);
                                    }, 0);
                                }
                            }

                            return {
                                cancel: function() {
                                    var i, len;
                                    $timeout.cancel(timeout);
                                    timeout = null;
                                    for (i = 0, len = callbacks.length; i < len; i++) {
                                        if (callbacks[i] === callback)
                                            callbacks[i] = null;
                                    }
                                }
                            };
                        };

                        var cache = "cadv1-" + (node || null);
                        self.series = CockpitMetrics.series(interval, cache, self.fetch);
                    }

                    return {
                        new_cadvisor: function(node) {
                            return new CAdvisor(node);
                        },
                    };
                }
            ])

            .factory('ServiceGrid', [
                "CAdvisorSeries",
                "CockpitMetrics",
                "kubeSelect",
                "kubeLoader",
                function ServiceGrid(CAdvisorSeries, CockpitMetrics, select, loader) {
                    function CockpitServiceGrid(until) {
                        var self = CockpitMetrics.grid(10000, 0, 0);

                        /* All the cadvisors that have been opened, one per host */
                        var cadvisors = { };

                        /* Service uids */
                        var services = { };

                        /* The various rows being shown, override */
                        self.rows = [ ];
                        self.events = [ ];

                        var change_queued = false;
                        var current_metric = null;

                        var rows = {
                            cpu: { },
                            memory: { },
                            network: { }
                        };

                        var container_cpu = { };
                        var container_mem = { };
                        var container_rx = { };
                        var container_tx = { };

                        /* Track Pods and Services */
                        loader.listen(function() {
                            var changed = false;
                            var seen_services = {};
                            var seen_hosts = {};

                            /* Lookup all the services */
                            angular.forEach(select().kind("Service"), function(service) {
                                var spec = service.spec;
                                var meta = service.metadata;
                                var name = meta.name;

                                if (!spec || !spec.selector || name === "kubernetes" || name === "kubernetes-ro")
                                    return;

                                var uid = meta.uid;
                                var pods = select().kind("Pod")
                                        .namespace(meta.namespace || "")
                                        .label(spec.selector || {});

                                seen_services[uid] = true;
                                if (!services[uid])
                                    add_service(uid);

                                /* Lookup all the pods for each service */
                                angular.forEach(pods, function(pod) {
                                    var status = pod.status || { };
                                    var spec = pod.spec || { };
                                    var host = spec.nodeName;
                                    var container_ids = {};
                                    var containers = status.containerStatuses || [];
                                    var i;
                                    var mapped = services[uid];

                                    seen_hosts[host] = true;
                                    if (host && !cadvisors[host]) {
                                        add_cadvisor(host);
                                    }

                                    /* Note all the containers for that pod */
                                    for (i = 0; i < containers.length; i++) {
                                        var container = containers[i];
                                        var id = container.containerID;
                                        if (id && id.indexOf("docker://") === 0) {
                                            container_ids[id] = id;
                                            id = id.substring(9);
                                            container_ids[id] = id;
                                            if (!mapped || !mapped[id])
                                                changed = true;
                                        }
                                    }
                                    services[uid] = container_ids;
                                });
                            });

                            var k;
                            for (k in services) {
                                if (!seen_services[k]) {
                                    remove_service(k);
                                    changed = true;
                                }
                            }

                            for (k in cadvisors) {
                                if (!seen_hosts[k]) {
                                    remove_host(k);
                                    changed = true;
                                }
                            }

                            /* Notify for all rows */
                            if (changed)
                                self.sync();
                        }, until);

                        loader.watch("Pod", until);
                        loader.watch("Service", until);

                        function add_container(cadvisor, id) {
                            var cpu = self.add(cadvisor, [ id, "cpu", "usage", "total" ]);
                            container_cpu[id] = self.add(function(row, x, n) {
                                row_delta(cpu, row, x, n);
                            }, true);

                            container_mem[id] = self.add(cadvisor, [ id, "memory", "usage" ]);

                            var rx = self.add(cadvisor, [ id, "network", "rx_bytes" ]);
                            container_rx[id] = self.add(function(row, x, n) {
                                row_delta(rx, row, x, n);
                            }, true);

                            var tx = self.add(cadvisor, [ id, "network", "tx_bytes" ]);
                            container_tx[id] = self.add(function(row, x, n) {
                                row_delta(tx, row, x, n);
                            }, true);

                            self.sync();
                        }

                        function add_cadvisor(host) {
                            var cadvisor = CAdvisorSeries.new_cadvisor(host);
                            cadvisor.watch(function (ids) {
                                var i;
                                for (i = 0; i < ids.length; i++)
                                    add_container(cadvisor, ids[i]);
                            });

                            /* A dummy row to force fetching data from the cadvisor */
                            self.add(cadvisor, [ "unused-dummy" ]);

                            /* TODO: Handle cadvisor failure somehow */
                            cadvisors[host] = cadvisor;
                        }

                        function remove_host(host) {
                            var cadvisor = cadvisors[host];
                            if (cadvisor) {
                                delete cadvisors[host];
                                cadvisor.close();
                                cadvisor = null;
                                change_queued = true;
                            }
                        }

                        function add_service(uid) {
                            /* CPU needs summing of containers, and then delta between them */
                            rows.cpu[uid] = self.add(function(row, x, n) {
                                containers_sum(uid, container_cpu, row, x, n);
                            });

                            /* Memory row is pretty simple, just sum containers */
                            rows.memory[uid] = self.add(function(row, x, n) {
                                containers_sum(uid, container_mem, row, x, n);
                            });

                            /* Network sums containers, then sum tx and rx, and then delta */
                            var tx = self.add(function(row, x, n) {
                                containers_sum(uid, container_tx, row, x, n);
                            });
                            var rx = self.add(function(row, x, n) {
                                containers_sum(uid, container_rx, row, x, n);
                            });
                            rows.network[uid] = self.add(function(row, x, n) {
                                rows_sum([tx, rx], row, x, n);
                            });

                            change_queued = true;
                        }

                        function remove_service(uid) {
                            delete services[uid];
                            delete rows.network[uid];
                            delete rows.cpu[uid];
                            delete rows.memory[uid];
                            change_queued = true;
                        }

                        function rows_sum(input, row, x, n) {
                            var max = row.maximum || 0;
                            var value, i, v, j;
                            var len = input.length;

                            /* Calculate the sum of the rows */
                            for (i = 0; i < n; i++) {
                                value = undefined;
                                for (j = 0; j < len; j++) {
                                    v = input[j][x + i];
                                    if (v !== undefined) {
                                        if (value === undefined)
                                            value = v;
                                        else
                                            value += v;
                                    }
                                }

                                if (value !== undefined && value > max) {
                                    row.maximum = max = value;
                                    change_queued = true;
                                }

                                row[x + i] = value;
                            }
                        }

                        function row_delta(input, row, x, n) {
                            var i, last, res, value;
                            if (x > 0)
                                last = input[x - 1];

                            var max = row.maximum || 1;
                            for (i = 0; i < n; i++) {
                                value = input[x + i];
                                if (last === undefined || value === undefined) {
                                    res = undefined;
                                } else {
                                    res = (value - last);
                                    if (res < 0) {
                                        res = undefined;
                                    } else if (res > max) {
                                        row.maximum = max = res;
                                        change_queued = true;
                                    }
                                }
                                row[x + i] = res;
                                last = value;
                            }
                        }

                        function containers_sum(service, input, row, x, n) {
                            var id, rowc;
                            var subset = [];
                            var mapped = services[service];
                            if (mapped) {
                                for (id in mapped) {
                                    rowc = input[id];
                                    if (rowc)
                                        subset.push(rowc);
                                }
                            }
                            rows_sum(subset, row, x, n);
                        }

                        self.metric = function metric(type) {
                            if (type === undefined)
                                return current_metric;
                            if (rows[type] === undefined)
                                throw Error("unsupported metric type");

                            self.rows = [];
                            current_metric = type;

                            var service_uids = Object.keys(services);
                            var row, i;
                            var len = service_uids.length;
                            for (i = 0; i < len; i++) {
                                row = rows[type][service_uids[i]];
                                if (row !== undefined) {
                                    self.rows.push(row);
                                    row.uid = service_uids[i];
                                }
                            }

                            var event = new CustomEvent("changed", {
                                bubbles: false,
                                cancelable: false,
                                detail: null
                            });
                            self.dispatchEvent(event, null);
                        };

                        var base_close = self.close;
                        self.close = function close() {
                            var hosts = Object.keys(cadvisors);
                            var i;
                            for (i = 0; i < hosts.length; i++) {
                                var k = hosts[i];
                                var cadvisor = cadvisors[k];
                                if (cadvisor) {
                                    delete cadvisors[k];
                                    cadvisor.close();
                                    cadvisor = null;
                                }
                            }
                            base_close.apply(self);
                        };

                        self.addEventListener("notify", function () {
                            if (change_queued) {
                                change_queued = false;
                                self.metric(current_metric);
                            }
                        });

                        return self;
                    }

                    return {
                        new_grid: function (until) {
                            return new CockpitServiceGrid(until);
                        }
                    };
                }
            ])

            .directive('kubernetesServiceGraph', [
                "ServiceGrid",
                "KubeTranslate",
                "KubeFormat",
                function kubernetesServiceGraph(ServiceGrid, KubeTranslate, KubeFormat) {
                    const _ = KubeTranslate.gettext;

                    function service_graph($scope, selector, highlighter) {
                        var grid = ServiceGrid.new_grid($scope);
                        var outer = d3.select(selector);

                        var highlighted = null;

                        /* Various tabs */

                        var tabs = {
                            cpu: {
                                label: _("CPU"),
                                step: 1000 * 1000 * 1000 * 10,
                                formatter: function(v) { return (v / (100 * 1000 * 1000)) + "%" }
                            },
                            memory: {
                                label: _("Memory"),
                                step: 1024 * 1024 * 64,
                                formatter: function(v) { return KubeFormat.formatBytes(v) }
                            },
                            network: {
                                label: _("Network"),
                                step: 1000 * 1000 * 10,
                                formatter: function(v) { return KubeFormat.formatBitsPerSec((v / 10), "Mbps") }
                            }
                        };

                        outer.append("ul")
                                .attr("class", "nav nav-tabs")
                                .selectAll("li")
                                .data(Object.keys(tabs))
                                .enter()
                                .append("li")
                                .attr("data-metric", function(d) { return d })
                                .append("a")
                                .text(function(d) { return tabs[d].label });

                        function metric_tab(tab) {
                            outer.selectAll("ul li")
                                    .attr("class", function(d) { return tab === d ? "active" : null });
                            grid.metric(tab);
                        }

                        outer.selectAll("ul li")
                                .on("click", function() {
                                    metric_tab(d3.select(this).attr("data-metric"));
                                });

                        metric_tab("cpu");

                        /* The main svg graph stars here */

                        var margins = {
                            top: 12,
                            right: 15,
                            bottom: 40,
                            left: 60
                        };

                        var colors = d3.scale.category20();

                        var element = d3.select(selector).append("svg");
                        var stage = element.append("g")
                                .attr("transform", "translate(" + margins.left + "," + margins.top + ")");

                        var y = d3.scale.linear();
                        var y_axis = d3.svg.axis()
                                .scale(y)
                                .ticks(5)
                                .orient("left");
                        var y_group = stage.append("g")
                                .attr("class", "y axis");

                        var x = d3.scale.linear();
                        var x_axis = d3.svg.axis()
                                .scale(x)
                                .orient("bottom");
                        var x_group = stage.append("g")
                                .attr("class", "x axis");

                        var offset = 0;

                        var line = d3.svg.line()
                                .defined(function(d) { return d !== undefined })
                                .x(function(d, i) { return x((grid.beg + i) - offset) })
                                .y(function(d, i) { return y(d) });

                        /* Initial display: 1024 px, 5 minutes of data */
                        var factor = 300000 / 1024;
                        var width = 300;
                        var height = 300;

                        var rendered = false;
                        window.setTimeout(function() {
                            rendered = true;
                            adjust();
                        }, 1);

                        function ceil(value, step) {
                            var d = value % step;
                            if (value === 0 || d !== 0)
                                value += (step - d);
                            return value;
                        }

                        function jump() {
                            var interval = grid.interval;
                            var w = (width - margins.right) - margins.left;
                            /* This doesn't yet work for an arbitary ponit in time */
                            var now = new Date().getTime();
                            var end = Math.floor(now / interval);
                            var beg = end - Math.floor((factor * w) / interval);
                            offset = beg;
                            grid.move(beg, end);
                        }

                        function adjust() {
                            if (!rendered)
                                return;

                            element
                                    .attr("width", width)
                                    .attr("height", height);

                            var w = (width - margins.right) - margins.left;
                            var h = (height - margins.top) - margins.bottom;

                            var metric = grid.metric();
                            var interval = grid.interval;

                            /* Calculate our maximum value, hopefully rows are tracking this for us */
                            var rows = grid.rows;
                            var maximum = 0;
                            var i, max;
                            var len = rows.length;
                            for (i = 0; i < len; i++) {
                                if (rows[i].maximum !== undefined)
                                    max = rows[i].maximum;
                                else
                                    max = d3.max(rows[i]);
                                if (max > maximum)
                                    maximum = Math.ceil(max);
                            }

                            /* This doesn't yet work for an arbitary ponit in time */
                            var end = Math.floor((factor * w) / interval);
                            x.domain([0, end]).range([0, w]);
                            y.domain([0, ceil(maximum, tabs[metric].step)]).range([h, 0]);

                            /* The ticks are inverted backwards */
                            var tsc = d3.scale.linear().domain([0, end])
                                    .range([end, 0]);

                            /* Calculate ticks every 60 seconds in past */
                            var ticks = [];
                            for (i = 6; i < end; i += 6)
                                ticks.push(Math.round(tsc(i)));

                            /* Make x-axis ticks into grid of right width */
                            x_axis
                                    .tickValues(ticks)
                                    .tickSize(-h, -h)
                                    .tickFormat(function(d) {
                                        d = Math.round(tsc.invert(d));
                                        return (d / 6) + " min";
                                    });

                            /* Re-render the X axis. Note that we also
                     * bump down the labels a bit. */
                            x_group
                                    .attr("transform", "translate(0," + h + ")")
                                    .call(x_axis)
                                    .selectAll("text")
                                    .attr("y", "10px");

                            /* Turn the Y axis ticks into a grid */
                            y_axis
                                    .tickSize(-w, -w)
                                    .tickFormat(tabs[metric].formatter);

                            y_group
                                    .call(y_axis)
                                    .selectAll("text")
                                    .attr("x", "-10px");

                            jump();
                        }

                        function notified() {
                            var rows = grid.rows;

                            var series = stage.selectAll("path.line")
                                    .data(rows, function(d, i) { return i });

                            series
                                    .style("stroke", function(d, i) { return colors(i) })
                                    .attr("d", function(d) { return line(d) })
                                    .classed("highlight", function(d) { return d.uid === highlighted });

                            series.enter().append("path")
                                    .attr("class", "line")
                                    .on("mouseover", function() {
                                        highlighter(d3.select(this).datum().uid);
                                    })
                                    .on("mouseout", function() {
                                        highlighter(null);
                                    });
                            series.exit().remove();
                        }

                        grid.addEventListener('notify', notified);

                        function changed() {
                            adjust();
                            notified();
                        }

                        grid.addEventListener('changed', changed);

                        function resized() {
                            width = selector.offsetWidth - 10;
                            if (width < 0)
                                width = 0;
                            adjust();
                        }

                        window.addEventListener('resize', resized);
                        resized();

                        var timer = window.setInterval(function () {
                            if (!width)
                                resized();
                            else
                                jump();
                        }, grid.interval);

                        return {
                            highlight: function highlight(uid) {
                                highlighted = uid;
                                notified();
                            },
                            close: function close() {
                                if (timer)
                                    window.clearInterval(timer);
                                timer = null;
                                window.removeEventListener('resize', resized);
                                grid.removeEventListener('notify', notified);
                                grid.removeEventListener('changed', changed);
                                grid.close();
                            }
                        };
                    }

                    return {
                        restrict: 'E',
                        link: function($scope, element, attributes) {
                            var graph = service_graph($scope, element[0], function(uid) {
                                $scope.$broadcast('highlight', uid);
                                $scope.$digest();
                            });
                            $scope.$on("highlight", function(ev, uid) {
                                graph.highlight(uid);
                            });
                            element.on('$destroy', function() {
                                graph.close();
                                graph = null;
                            });
                        }
                    };
                }
            ]);
}());
