/*
 * 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/>.
 */

import $ from "jquery";
import cockpit from "cockpit";

import { mustache } from "mustache";
import "patterns";

import { docker } from "./docker";
import * as bar from "./bar";
import { journal } from "journal";
import moment from "moment";
moment.locale(cockpit.language);

const _ = cockpit.gettext;

export var util = { };

util.resource_debug = function resource_debug() {
    if (window.debugging == "all" || window.debugging == "resource")
        console.debug.apply(console, arguments);
};

util.docker_debug = function docker_debug() {
    if (window.debugging == "all" || window.debugging == "docker")
        console.debug.apply(console, arguments);
};

util.quote_cmdline = function quote_cmdline(cmds) {
    return docker.quote_cmdline(cmds || []);
};

util.unquote_cmdline = function unquote_cmdline(string) {
    return docker.unquote_cmdline(string);
};

util.render_container_cmdline = function render_container_cmdline (container) {
    // We do our own quoting in preference to using container.Command.
    // We do this for consistency, and also to avoid bugs in how
    // Docker creates container.Command.  Docker doesn't escape quote
    // characters, for example.

    if (container.Config)
        return util.quote_cmdline((container.Config.Entrypoint || []).concat(container.Config.Cmd || []));
    else
        return container.Command;
};

/*
 * Recent versions of docker have a 'Status' field in the state,
 * but earlier versions have distinct fields which we need to combine.
 */
util.render_container_status = function render_container_status(state) {
    if (state.Status)
        return state.Status;
    if (state.Running)
        return "running";
    if (state.Paused)
        return "paused";
    if (state.Restarting)
        return "restarting";
    if (state.FinishedAt && state.FinishedAt.indexOf("0001") === 0)
        return "created";
    return "exited";
};

util.render_container_name = function render_container_name (name) {
    if (name.length > 0 && name[0] == "/")
        return name.slice(1);
    else
        return name;
};

util.render_container_state = function render_container_state (state) {
    if (state.Running) {
        var momentDate = moment(state.StartedAt);
        return cockpit.format(_("Up since $0"), momentDate.isValid()
            ? momentDate.calendar() : state.startedAt);
    }
    return cockpit.format(_("Exited $ExitCode"), state);
};

util.render_container_restart_policy = function render_restart_policy(policy) {
    switch (policy.Name) {
    case "no":
        return _("No");
    case "on-failure":
        var text = cockpit.ngettext("On failure, retry $0 time", "On failure, retry $0 times", policy.MaximumRetryCount);
        return cockpit.format(text, policy.MaximumRetryCount);
    case "always":
        return _("Always");
    case "unless-stopped":
        return _("Unless Stopped");
    default: /* Keeping this here just in case. http://stackoverflow.com/a/4878800 */
        return policy.Name.replace('-', ' ').replace(/\w\S*/g, function(txt) {
            return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
        });
    }
};

util.multi_line = function multi_line(strings) {
    return strings.map(function (str) { return mustache.render("{{.}}", str) }).join('<br/>');
};

util.format_cpu_shares = function format_cpu_shares(priority) {
    if (!priority)
        return _("default");
    return cockpit.format(_("$0 shares"), Math.round(priority));
};

util.format_cpu_usage = function format_cpu_usage(usage) {
    if (usage === undefined || isNaN(usage))
        return "";
    return Math.round(usage) + "%";
};

util.update_memory_bar = function update_memory_bar(bar, usage, limit) {
    var parts = [usage];
    if (limit)
        parts.push(limit);
    $(bar)
            .attr("value", parts.join("/"))
            .toggleClass("bar-row-danger", !!(limit && usage > 0.9 * limit));
};

util.format_memory_and_limit = function format_memory_and_limit(usage, limit) {
    if (usage === undefined || isNaN(usage))
        return "";

    var mtext = "";
    var units = 1024;
    var parts;
    if (limit) {
        parts = cockpit.format_bytes(limit, units, true);
        mtext = " / " + parts.join(" ");
        units = parts[1];
    }

    if (usage) {
        parts = cockpit.format_bytes(usage, units, true);
        if (mtext)
            return parts[0] + mtext;
        else
            return parts.join(" ");
    } else {
        return "?" + mtext;
    }
};

util.insert_table_sorted = function insert_table_sorted(table, row) {
    util.insert_table_sorted_generic(table, row, function(row1, row2) {
        return row1.text().localeCompare(row2.text());
    });
};

util.insert_table_sorted_generic = function insert_table_sorted_generic(table, row, cmp) {
    var rows = $(table).find("tbody tr");
    for (var j = 0; j < rows.length; j++) {
        if (cmp($(rows[j]), row) > 0) {
            $(row).insertBefore(rows[j]);
            row = null;
            break;
        }
    }
    if (row !== null)
        $(table).find("tbody")
                .append(row);
};

util.setup_danger_button = function setup_danger_button(id, parent_id, callback) {
    var danger_button = $('<button class="pf-c-button pf-m-secondary btn-sm enable-danger">')
            .append('<span class="fa fa-check">')
            .toggle(false)
            .on("click", callback);
    $(id + ' th.container-column-actions').append(danger_button);
    $(parent_id)[0].addEventListener("click", function(ev) {
        if ($(ev.target).parents(id).length === 0 &&
            $(id + ' button.enable-danger').hasClass('active'))
            callback();
    }, true);
};

util.render_container = function render_container(client, $panel,
    prefix, id, container, danger_mode) {
    // Docker ID can contain funny characters such as ":" so
    // we take care not to embed them into jQuery query
    // strings or HTML.

    var tr = $(document.getElementById(prefix + id));

    if (!container) {
        tr.remove();
        if (!$panel.find('table > tbody > tr').length) {
            $panel.find('button.enable-danger').toggle(false);
        }
        return;
    }

    var cputext;
    var memuse, memlimit;
    var membar, memtext, memtextstyle;

    if (container.State && container.State.Running) {
        cputext = util.format_cpu_usage(container.CpuUsage);

        memuse = container.MemoryUsage;
        memlimit = container.MemoryLimit;
        memtext = util.format_memory_and_limit(memuse, memlimit);

        membar = true;
        memtextstyle = { color: 'inherit' };
    } else {
        cputext = "";
        membar = false;
        memtext = _("Stopped");
        memtextstyle = { color: 'grey', 'text-align': 'right' };
    }

    var added = false;
    if (!tr.length) {
        $panel.find('button.enable-danger').toggle(true);
        var img_waiting = $('<div class="spinner">');
        var btn_delete = $('<button class="pf-c-button pf-m-danger btn-delete">')
                .append('<span class="pficon pficon-delete">')
                .on("click", function() {
                    var self = this;
                    $(self).hide()
                            .siblings("div.spinner")
                            .show();
                    util.docker_container_delete(client, id, function() { }, function () {
                        $(self).show()
                                .siblings("div.spinner")
                                .hide();
                    });
                    return false;
                });
        var btn_play = $('<button class="pf-c-button pf-m-secondary btn-sm play-button">')
                .append('<span class="fa fa-play">')
                .on("click", function() {
                    $(this).hide()
                            .siblings("div.spinner")
                            .show();
                    client.start(id)
                            .fail(function(ex) {
                                util.handle_scope_start_container(client, id, ex.message);
                            });
                    return false;
                });
        var btn_stop = $('<button class="pf-c-button pf-m-secondary btn-sm stop-button">')
                .append('<span class="fa fa-stop"')
                .on("click", function() {
                    $(this).hide()
                            .siblings("div.spinner")
                            .show();
                    client.stop(id)
                            .fail(function(ex) {
                                util.show_unexpected_error(ex);
                            });
                    return false;
                });
        tr = $('<tr>', { id: prefix + id }).append(
            $('<td class="container-column-name">'),
            $('<td class="container-column-image">'),
            $('<td class="container-column-command">'),
            $('<td class="container-column-cpu">'),
            $('<td class="container-column-memory-graph">').append(bar.create("containers-containers")),
            $('<td class="container-column-memory-text">'),
            $('<td class="container-column-danger cell-buttons">').append(btn_delete, img_waiting),
            $('<td class="container-column-actions cell-buttons">').append(btn_play, btn_stop, img_waiting.clone()));

        tr.on('click', function(event) {
            cockpit.location.go([id]);
        });

        added = true;
    }

    var row = tr.children("td");
    $(row[0]).text(util.render_container_name(container.Name));

    var image = container.Image;
    if (container.ImageID && image == container.ImageID)
        image = docker.truncate_id(image);
    $(row[1]).text(image);

    $(row[2]).text(util.render_container_cmdline(container));
    $(row[3]).text(cputext);
    util.update_memory_bar($(row[4]).children("div")
            .toggle(membar), memuse, memlimit);
    $(row[5])
            .css(memtextstyle)
            .text(memtext);

    var waiting = id in client.waiting;
    $(row[6]).children("div.spinner")
            .toggle(waiting);
    $(row[6]).children("button.btn-delete")
            .toggle(!waiting)
            .prop('disabled', container.State.Running);

    var title = (waiting || container.State.Running) ? "You can only delete<br/> stopped containers" : "Delete immediately";

    $(row[6]).children("button.btn-delete")
            .tooltip('destroy')
            .attr("title", title)
            .tooltip({ html: true });

    $(row[7]).children("div.spinner")
            .toggle(waiting);
    $(row[7]).children("button.play-button")
            .toggle(!waiting && !container.State.Running);
    $(row[7]).children("button.stop-button")
            .toggle(!waiting && container.State.Running);

    $(row[6]).toggle(danger_mode);
    $(row[7]).toggle(!danger_mode);

    tr.toggleClass("unimportant", !container.State.Running);

    if (added)
        util.insert_table_sorted($panel.find('table'), tr);

    bar.update();
};

/* Slider/text/checkbox interaction happens here */
function Slider(sel, min, max, parse, format) {
    var self = this;
    var slider, input, check;
    var updating = false;
    var data;

    /* Logarithmic scale */
    if (min < 0)
        min = 0;
    if (max < 0)
        max = 0;
    var minv = Math.log(min);
    var maxv = Math.log(max);
    var scale = (maxv - minv);

    function limit(val) {
        if (val < min)
            val = min;
        else if (val > max)
            val = max;
        return val;
    }

    function slider_load() {
        if (check.checked)
            data = limit(Math.round(Math.exp(minv + scale * slider.value)));
        else
            data = undefined;
    }

    function slider_update() {
        updating = true;
        if (data !== undefined)
            $(slider).prop("value", (Math.log(data) - minv) / scale);
        $(slider)
                .attr("disabled", !check.checked)
                .trigger("change");
        updating = false;
    }

    function text_load() {
        var val;
        if (check.checked)
            val = limit(parse($(input).val()));
        else
            val = undefined;
        if (isNaN(val))
            val = undefined;
        data = val;
    }

    function text_update() {
        updating = true;
        if (data !== undefined)
            $(input).val(format(data));
        $(input).attr("disabled", data === undefined);
        updating = false;
    }

    function check_load() {
        if (!check.checked)
            data = undefined;
    }

    function check_update() {
        updating = true;
        $(check).prop("checked", data !== undefined);
        updating = false;
    }

    /* Slider to change CPU priority */
    slider = sel.find("div.slider")
            .on('change', function() {
                if (updating)
                    return;
                slider_load();
                text_update();
            })[0];

    /* Number value of CPU priority */
    input = sel.find("input.size-text-ct")
            .on('change', function() {
                if (updating)
                    return;
                text_load();
                slider_update();
            })[0];

    /* Default checkbox */
    check = sel.find("input[type='checkbox']")
            .on('change', function() {
                if (updating)
                    return;
                check_load();
                if (this.checked)
                    text_load();
                slider_update();
                text_update();
            })[0];

    Object.defineProperty(self, "value", {
        get: function() {
            return data;
        },
        set: function(v) {
            data = v;
            check_update();
            slider_update();
            text_update();
        }
    });

    Object.defineProperty(self, "max", {
        get: function() {
            return max;
        },
        set: function(v) {
            if (v < 0)
                v = 0;
            max = v;
            maxv = Math.log(max);
            scale = (maxv - minv);
            if (slider)
                slider_update();
        }
    });

    return self;
}

/* Memory limit slider/checkbox interaction happens here */
util.MemorySlider = function MemorySlider(sel, min, max) {
    function parse(val) {
        return parseInt(val, 10) * 1024 * 1024;
    }

    function format(val) {
        return cockpit.format_bytes(val, "MiB", true)[0];
    }

    return new Slider(sel, min, max, parse, format);
};

/* CPU priority slider/checkbox interaction happens here */
util.CpuSlider = function CpuSlider(sel, min, max) {
    function parse(val) {
        return parseInt(val, 10);
    }

    function format(val) {
        return String(val);
    }

    return new Slider(sel, min, max, parse, format);
};

util.docker_container_delete = function docker_container_delete(docker_client, container_id, on_success, on_failure) {
    docker_client.rm(container_id)
            .fail(function(ex) {
            /* if container is still running, ask user to force delete */
                if (ex.message.indexOf('remove a running container') > -1) {
                    var container_info = docker_client.containers[container_id];
                    var msg;
                    if (container_info.State.Running) {
                        msg = _("Container is currently running.");
                    } else {
                        msg = _("Container is currently marked as not running, but regular stopping failed.") +
                        " " + _("Error message from Docker:") +
                        " '" + ex.message + "'";
                    }
                    var name;
                    if (container_info.Name)
                        name = container_info.Name;
                    else
                        name = container_id;
                    if (name.charAt(0) === '/')
                        name = name.substring(1);
                    util.confirm(cockpit.format(_("Please confirm forced deletion of $0"), name),
                                 msg,
                                 _("Force Delete"))
                            .done(function () {
                                docker_client.rm(container_id, true)
                                        .fail(function(ex) {
                                            util.show_unexpected_error(ex);
                                            on_failure();
                                        })
                                        .done(on_success);
                            })
                            .fail(on_failure);
                    return;
                }
                util.show_unexpected_error(ex);
                on_failure();
            })
            .done(on_success);
};

/* if error message points to leftover scope, try to resolve the issue */
util.handle_scope_start_container = function handle_scope_start_container(docker_client, container_id, error_message, on_success, on_failure) {
    var end_phrase = '.scope already exists';
    var idx_end = error_message.indexOf(end_phrase);
    /* HACK: workaround for https://github.com/docker/docker/issues/7015 */
    if (idx_end > -1) {
        var start_phrase = 'Unit docker-';
        var idx_start = error_message.indexOf(start_phrase) + start_phrase.length;
        var docker_container = error_message.substring(idx_start, idx_end);
        cockpit.spawn(["systemctl", "stop", "docker-" + docker_container + ".scope"], { superuser: "try" })
                .done(function () {
                    docker_client.start(container_id)
                            .fail(function(ex) {
                                if (on_failure)
                                    on_failure();
                            })
                            .done(function() {
                                if (on_success)
                                    on_success();
                            });
                })
                .fail(function (error) {
                    util.show_unexpected_error(cockpit.format(_("Failed to stop Docker scope: $0"), error));
                    if (on_failure)
                        on_failure();
                });
        return;
    }
    util.show_unexpected_error(error_message);
    if (on_failure)
        on_failure();
};

util.show_unexpected_error = function show_unexpected_error(error) {
    $("#error-popup-message").text(error.message || error || "???");
    $('.modal[role="dialog"]').modal('hide');
    $('#error-popup').modal('show');
};

util.confirm = function confirm(title, body, action_text) {
    var deferred = $.Deferred();

    $('#confirmation-dialog-title').text(title);
    if (typeof body == "string")
        $('#confirmation-dialog-body').text(body);
    else
        $('#confirmation-dialog-body').html(body);
    $('#confirmation-dialog-confirm').text(action_text);

    function close() {
        $('#confirmation-dialog button').off('click');
        $('#confirmation-dialog').modal('hide');
    }

    $('#confirmation-dialog-confirm').click(function () {
        close();
        deferred.resolve();
    });

    $('#confirmation-dialog-cancel').click(function () {
        close();
        deferred.reject();
    });

    $('#confirmation-dialog').modal('show');
    return deferred.promise();
};

util.delete_image_confirm = function confirm(client, image) {
    var deferred = $.Deferred();
    var image_id = image.image_id || image.Id;
    var image_name = image.name || util.render_container_name(image.RepoTags[0]);
    var $dialog = $('#delete-image-confirmation-dialog');

    $dialog.find('#delete-image-confirmation-dialog-title').text(
        cockpit.format(_("Please confirm deletion of $0"), image_name));
    $dialog.find('#delete-image-confirmation-dialog-containers .listing-ct-body').empty();
    client.containers_for_image(image_id).then(function(containers) {
        var running_containers = [];

        $(containers).each(function(index, value) {
            var container = client.containers[value.Id];
            var $row = $('<tr />', { class: 'listing-ct-item' })
                    .append($('<td />').text(container.Name.startsWith('/')
                        ? container.Name.substring(1) : container.Name))
                    .append($('<td />').text(util.render_container_status(container.State)));
            $dialog.find('#delete-image-confirmation-dialog-containers .listing-ct-body')
                    .append($row);
            if (container.State.Running)
                running_containers.push(container.Id);
        });

        if (containers.length > 0) {
            $dialog.find('#delete-image-confirmation-dialog-body').text(
                _("The following containers depend on this image and will become unusable."));
            $dialog.find('#delete-image-confirmation-dialog-containers').show();
        } else {
            $dialog.find('#delete-image-confirmation-dialog-body').empty();
            $dialog.find('#delete-image-confirmation-dialog-containers').hide();
        }

        if (running_containers.length > 0)
            $dialog.find('#delete-image-confirmation-dialog-confirm').text(_("Stop and delete"));
        else
            $dialog.find('#delete-image-confirmation-dialog-confirm').text(_("Delete"));

        function close() {
            $dialog.find('button').off('click');
            $dialog.modal('hide');
        }

        $dialog.find('#delete-image-confirmation-dialog-confirm').click(function () {
            close();
            deferred.resolve(running_containers, containers.length > 0);
        });

        $dialog.find('#delete-image-confirmation-dialog-cancel').click(function () {
            close();
            deferred.reject();
        });

        $dialog.modal('show');
    });
    return deferred.promise();
};

util.find_container_log = function find_container_log(client, entry, resultCallback) {
    client.call(entry, "org.freedesktop.DBus.Properties", "GetAll",
                ["org.freedesktop.Problems2.Entry"])
            .fail(function (error) {
                console.log(error);
            })
            .done(function (sem_result) {
                client.call(entry, "org.freedesktop.Problems2.Entry",
                            "ReadElements", [['container_id'], 0x4])
                        .fail(function (error) {
                            console.log(error);
                        })
                        .done(function (elem_result) {
                            if ('container_id' in elem_result[0]) {
                                var problem_id = sem_result[0].ID.v;
                                // Wait for a while, until ABRT processes the problem and writes it into journal
                                window.setTimeout(util.problem_log, 2000, 0, problem_id, elem_result[0].container_id.v, resultCallback);
                            }
                        });
            });
};

util.problem_log = function problem_log (poll_count, problem_id, c_id, resultCallback) {
    var url = null;
    var message = "";
    var match = [];
    var log_found = false;
    match.push('SYSLOG_IDENTIFIER=abrt-notification');
    match.push('PROBLEM_DIR=' + problem_id);
    journal.journalctl(match, { follow: false, reverse: true, all: true })
            .stream(function(entries) {
                log_found = true;
                // Only first entry is enough, since others are only previous occurrences
                url = cockpit.location.encode(entries[0].__CURSOR);
                url = "/system/logs#" + url;
                if (entries[0].PROBLEM_REASON)
                    message = entries[0].PROBLEM_REASON;
                else
                    message = entries[0].MESSAGE;

                resultCallback(c_id, url, message);
            })
            .done(function() {
            // Try pooling for minute, then give up
                if (!log_found) {
                    if (poll_count < 30) {
                        window.setTimeout(util.problem_log, 2000, ++poll_count, problem_id, c_id, resultCallback);
                    } else {
                        console.warn("No journal log found for problem " + problem_id);
                    }
                }
            });
};

util.find_all_problems = function find_problems(problems, client, service, resultCallback) {
    problems.wait(function() {
        try {
            service.GetProblems(0, {})
                    .done(function(problem_paths, options) {
                        for (var i in problem_paths) {
                            util.find_container_log(client, problem_paths[i], resultCallback);
                        }
                    });
        } catch (err) {
            /* ignore errors */
        }
    });
};
