/*
 * 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');
    require("angular-route");
    require('angular-dialog.js');

    require('./kube-client');
    require('./date');
    require('./tags');
    require('./policy');

    require('registry-image-widgets/dist/image-widgets.js');

    require('../views/images-page.html');
    require('../views/imagestream-page.html');
    require('../views/image-page.html');
    require('../views/imagestream-delete.html');
    require('../views/imagestream-modify.html');
    require('../views/imagestream-modify.html');
    require('../views/image-delete.html');

    /*
     * Executes callback for each stream.status.tag[x].item[y]
     * in a stream. Similar behavior to angular.forEach()
     */
    function imagestreamEachTagItem(stream, callback, context) {
        var i, il, items;
        var t, tl;
        var tags = (stream.status || {}).tags || [];
        for (t = 0, tl = tags.length; t < tl; t++) {
            items = (tags[t].items) || [];
            for (i = 0, il = items.length; i < il; i++)
                callback.call(context || null, tags[t], items[i]);
        }
    }

    function identifier(imagestream, tag) {
        var id = imagestream.metadata.namespace + "/" + imagestream.metadata.name;
        if (tag)
            id += ":" + tag.name;
        return id;
    }

    angular.module('registry.images', [
        'ngRoute',
        'ui.cockpit',
        'kubeClient',
        'kubernetes.date',
        'registry.tags',
        'registryUI.images',
    ])

            .config([
                '$routeProvider',
                function($routeProvider) {
                    $routeProvider
                            .when('/images/:namespace?', {
                                templateUrl: 'views/images-page.html',
                                controller: 'ImagesCtrl'
                            })
                            .when('/images/:namespace/:target', {
                                controller: 'ImageCtrl',
                                templateUrl: function(params) {
                                    var target = params['target'] || '';
                                    if (target.indexOf(':') === -1)
                                        return 'views/imagestream-page.html';
                                    else
                                        return 'views/image-page.html';
                                }
                            });
                }
            ])

            .factory('registryListingScopeSetup', [
                'imageData',
                'imageActions',
                'projectData',
                'kubeSelect',
                '$location',
                function (data, actions, projectData, select, $location) {
                    return function($scope, inPage) {
                        function imageByTag (tag) {
                            if (tag && tag.items && tag.items.length)
                                return select().kind("Image")
                                        .name(tag.items[0].image)
                                        .one();
                        }

                        function deleteImageStream(stream) {
                            var promise = actions.deleteImageStream(stream);

                            /* If the promise is successful, redirect to another page */
                            promise.then(function() {
                                $location.path($scope.viewUrl('images'));
                            });

                            return promise;
                        }

                        function deleteTag(stream, tag) {
                            var promise = actions.deleteTag(stream, tag);

                            /* If the promise is successful, redirect to another page */
                            promise.then(function() {
                                var parts = [ "images", stream.metadata.namespace, stream.metadata.name ];
                                $location.path("/" + parts.map(encodeURIComponent).join("/"));
                            });

                            return promise;
                        }

                        /* All the actions available on the $scope */
                        angular.extend($scope, actions);
                        angular.extend($scope, data);

                        $scope.sharedImages = projectData.sharedImages;
                        $scope.imageTagNames = data.imageTagNames;
                        $scope.imageByTag = imageByTag;

                        if (inPage) {
                            $scope.deleteTag = deleteTag;
                            $scope.deleteImageStream = deleteImageStream;
                        }

                        $scope.actions = {
                            modifyImageStream: $scope.modifyImageStream,
                            deleteImageStream: $scope.deleteImageStream,
                            deleteTag: $scope.deleteTag,
                            modifyProject: $scope.modifyProject,
                        };
                    };
                }
            ])

            .controller('ImagesCtrl', [
                '$scope',
                '$location',
                'imageData',
                'imageActions',
                'projectData',
                'kubeLoader',
                'registryListingScopeSetup',
                'filterService',
                function($scope, $location, data, actions, projectData, loader, registryListingScopeSetup) {
                    $scope.sharedImages = projectData.sharedImages;

                    /* Watch all the images in current namespace */
                    data.watchImages($scope);

                    $scope.imagestreams = data.allStreams();
                    loader.listen(function() {
                        $scope.imagestreams = data.allStreams();
                    }, $scope);

                    $scope.$on("activate", function(ev, imagestream, tag) {
                        ev.preventDefault();
                        $location.path('/images/' + identifier(imagestream, tag));
                    });

                    registryListingScopeSetup($scope, false);
                }
            ])

    /*
     * Note that we use the same controller for both the ImageStream
     * and the Image view. This is because ngRoute can't special case
     * routes based on the colon we use to differentiate the two in
     * the path.
     *
     * ie: cockpit/ws vs. cockpit/ws:latest
     *
     * The |kind| on the scope tells us which is which.
     */
            .controller('ImageCtrl', [
                '$scope',
                '$location',
                '$routeParams',
                'kubeSelect',
                'kubeLoader',
                'KubeDiscoverSettings',
                'imageData',
                'imageActions',
                'projectData',
                'projectPolicy',
                'registryListingScopeSetup',
                function($scope, $location, $routeParams, select, loader, discoverSettings, data, actions, projectData, projectPolicy, registryListingScopeSetup) {
                    var target = $routeParams["target"] || "";
                    var pos = target.indexOf(":");

                    /* colon contains a tag name, only set if we're looking at an image */
                    var namespace = $routeParams["namespace"] || "";
                    var name, tagname;
                    if (pos === -1) {
                        $scope.kind = "ImageStream";
                        name = target;
                        tagname = null;
                    } else {
                        $scope.kind = "Image";
                        name = target.substr(0, pos);
                        tagname = target.substr(pos + 1);
                    }

                    registryListingScopeSetup($scope, true);

                    /* There's no way to watch a single item ... so watch them all :( */
                    data.watchImages($scope);

                    loader.listen(function() {
                        $scope.stream = select().kind("ImageStream")
                                .namespace(namespace)
                                .name(name)
                                .one();
                        $scope.image = $scope.config = $scope.layers = $scope.labels = $scope.tag = null;

                        imagestreamEachTagItem($scope.stream || {}, function(tag, item) {
                            if (tag.tag === tagname)
                                $scope.tag = tag;
                        });

                        if ($scope.tag)
                            $scope.image = $scope.imageByTag($scope.tag);
                        if ($scope.image) {
                            $scope.names = data.imageTagNames($scope.image);
                            $scope.config = data.imageConfig($scope.image);
                            $scope.layers = data.imageLayers($scope.image);
                            $scope.labels = data.imageLabels($scope.image);
                        }
                    }, $scope);

                    $scope.$on("activate", function(ev, imagestream, tag) {
                        ev.preventDefault();
                        $location.path('/images/' + identifier(imagestream, tag));
                    });

                    function updateShowDockerPushCommands() {
                        discoverSettings().then(function(settings) {
                            projectPolicy.subjectAccessReview(namespace, settings.currentUser, 'update', 'imagestreamimages')
                                    .then(function(allowed) {
                                        if (allowed != $scope.showDockerPushCommands) {
                                            $scope.showDockerPushCommands = allowed;
                                            $scope.$applyAsync();
                                        }
                                    });
                        });
                    }

                    // watch for project changes to update showDockerPushCommands, and initialize it
                    $scope.$on("$routeUpdate", updateShowDockerPushCommands);
                    updateShowDockerPushCommands();
                }
            ])

            .factory("imageData", [
                'kubeSelect',
                'kubeLoader',
                function(select, loader) {
                    var watching = false;

                    /* Called when we have to load images via imagestreams */
                    loader.listen(function(objects) {
                        for (var link in objects) {
                            if (objects[link].kind === "ImageStream")
                                handle_imagestream(objects[link]);
                            if (objects[link].kind === "Image")
                                handle_image(objects[link]);
                        }
                    });

                    function handle_imagestream(imagestream) {
                        var meta = imagestream.metadata || { };
                        var status = imagestream.status || { };
                        angular.forEach(status.tags || [ ], function(tag) {
                            angular.forEach(tag.items || [ ], function(item) {
                                var link = loader.resolve("Image", item.image);
                                if (link in loader.objects)
                                    return;

                                /* An interim object while we're loading */
                                var interim = { kind: "Image", apiVersion: "v1", metadata: { name: item.image } };
                                loader.handle(interim);

                                if (!watching)
                                    return;

                                var name = meta.name + "@" + item.image;
                                loader.load("ImageStreamImage", name, meta.namespace).then(function(resource) {
                                    var image = resource.image;
                                    if (image) {
                                        image.kind = "Image";
                                        loader.handle(image);
                                        handle_image(image);
                                    }
                                }, function(response) {
                                    var message = response.statusText || response.message || String(response);
                                    console.warn("couldn't load image: " + message);
                                    interim.metadata.resourceVersion = "invalid";
                                });
                            });
                        });
                    }

                    /*
             * Create a pseudo-item with kind DockerImageManifest for
             * each image with a dockerImageManifest that we see. Identical
             * name to the image itself.
             */
                    function handle_image(image) {
                        var item;
                        var manifest = image.dockerImageManifest;
                        if (manifest) {
                            manifest = JSON.parse(manifest);
                            angular.forEach(manifest.history || [], function(item) {
                                if (typeof item.v1Compatibility == "string")
                                    item.v1Compatibility = JSON.parse(item.v1Compatibility);
                            });
                            item = {
                                kind: "DockerImageManifest",
                                metadata: {
                                    name: image.metadata.name,
                                    selfLink: "/internal/manifests/" + image.metadata.name
                                },
                                manifest: manifest,
                            };
                            loader.handle(item);
                        }
                    }

                    /* Load images, but fallback to loading individually */
                    function watchImages(until) {
                        watching = true;
                        var a = loader.watch("images", until);
                        var b = loader.watch("imagestreams", until);

                        return {
                            cancel: function() {
                                a.cancel();
                                b.cancel();
                            }
                        };
                    }

                    /*
             * Filters selection to those with names that is
             * in the given TagEvent.
             */
                    select.register("taggedBy", function(tag) {
                        var i, len;
                        var results = { };
                        // catch condition when tag.items is null due to imagestream import error
                        if (!tag.items)
                            return select(null);
                        for (i = 0, len = tag.items.length; i < len; i++)
                            this.name(tag.items[i].image).extend(results);
                        return select(results);
                    });

                    /*
             * Filters selection to those with names that is in the first
             * item in the given TagEvent.
             */
                    select.register("taggedFirst", function(tag) {
                        var results = { };
                        if (!tag.items)
                            return select(null);
                        if (tag.items.length)
                            this.name(tag.items[0].image).extend(results);
                        return select(results);
                    });

                    /*
             * Filter that gets image streams for the given tag.
             */
                    select.register({
                        name: "containsTagImage",
                        digests: function(arg) {
                            var ret = [];
                            if (typeof arg == "string") {
                                ret.push(arg);
                            } else {
                                imagestreamEachTagItem(arg, function(tag, item) {
                                    ret.push(item.image + "");
                                });
                            }
                            return ret;
                        }
                    });

                    select.register("listTagNames", function(image_name) {
                        var names = [];
                        angular.forEach(this.containsTagImage(image_name), function(stream) {
                            imagestreamEachTagItem(stream, function(tag, item) {
                                if (!image_name || item.image == image_name)
                                    names.push(stream.metadata.namespace + "/" + stream.metadata.name + ":" + tag.tag);
                            });
                        });
                        return names;
                    });

                    /*
             * Filter that gets the config object for a docker based
             * image.
             */
                    select.register("dockerImageConfig", function() {
                        var results = { };
                        angular.forEach(this, function(image, key) {
                            var compat;
                            var layers = imageLayers(image) || { };
                            if (layers[0]) {
                                compat = layers[0].v1Compatibility;
                                if (compat && compat.config) {
                                    results[key] = compat.config;
                                    return;
                                }
                            }

                            var meta = image.dockerImageMetadata || { };
                            if (meta.Config)
                                results[key] = meta.Config;
                        });

                        return select(results);
                    });

                    /*
             * Filter that gets a dict of labels for a config
             * image.
             */
                    select.register("dockerConfigLabels", function() {
                        var results = { };
                        angular.forEach(this, function(config, key) {
                            var labels;
                            if (config)
                                labels = config.Labels;
                            if (labels)
                                results[key] = labels;
                        });
                        return select(results);
                    });

                    function imageLayers(image) {
                        if (!image)
                            return null;
                        var item = select().kind("DockerImageManifest")
                                .name(image.metadata.name)
                                .one();
                        if (item && item.manifest && item.manifest.schemaVersion === 1)
                            return item.manifest.history;
                        if (image.dockerImageLayers)
                            return image.dockerImageLayers;
                        return null;
                    }

                    /* HACK: We really want a metadata index here */
                    function configCommand(config) {
                        var result = [ ];
                        if (!config)
                            return "";
                        if (config.Entrypoint)
                            result.push.apply(result, config.Entrypoint);
                        if (config.Cmd)
                            result.push.apply(result, config.Cmd);
                        var string = result.join(" ");
                        if (config.User && config.User.split(":")[0] != "root")
                            return "$ " + string;
                        else
                            return "# " + string;
                    }

                    return {
                        watchImages: watchImages,
                        allStreams: function allStreams() {
                            return select().kind("ImageStream");
                        },
                        imageLayers: imageLayers,
                        imageConfig: function imageConfig(image) {
                            return select(image).dockerImageConfig()
                                    .one() || { };
                        },
                        imageTagNames: function imageTagNames(image) {
                            return select().kind("ImageStream")
                                    .listTagNames(image.metadata.name);
                        },
                        imageLabels: function imageLabels(image) {
                            var labels = select(image).dockerImageConfig()
                                    .dockerConfigLabels()
                                    .one();
                            if (labels && angular.equals({ }, labels))
                                labels = null;
                            return labels;
                        },
                        configCommand: configCommand,
                    };
                }
            ])

            .factory('imageActions', [
                '$modal',
                '$location',
                function($modal, $location) {
                    function deleteImageStream(stream) {
                        return $modal.open({
                            animation: false,
                            controller: 'ImageStreamDeleteCtrl',
                            templateUrl: 'views/imagestream-delete.html',
                            resolve: {
                                dialogData: function() {
                                    return { stream: stream };
                                }
                            },
                        }).result;
                    }

                    function createImageStream() {
                        return $modal.open({
                            animation: false,
                            controller: 'ImageStreamModifyCtrl',
                            templateUrl: 'views/imagestream-modify.html',
                            resolve: {
                                dialogData: function() {
                                    return { };
                                }
                            },
                        }).result;
                    }

                    function modifyImageStream(stream) {
                        return $modal.open({
                            animation: false,
                            controller: 'ImageStreamModifyCtrl',
                            templateUrl: 'views/imagestream-modify.html',
                            resolve: {
                                dialogData: function() {
                                    return { stream: stream };
                                }
                            },
                        }).result;
                    }

                    function deleteTag(stream, tag) {
                        var modal = $modal.open({
                            animation: false,
                            controller: 'ImageDeleteCtrl',
                            templateUrl: 'views/image-delete.html',
                            resolve: {
                                dialogData: function() {
                                    return { stream: stream, tag: tag };
                                }
                            },
                        });

                        return modal.result;
                    }

                    function modifyProject(project) {
                        $location.path("/projects/" + project);
                        return false;
                    }

                    return {
                        createImageStream: createImageStream,
                        modifyImageStream: modifyImageStream,
                        deleteImageStream: deleteImageStream,
                        deleteTag: deleteTag,
                        modifyProject: modifyProject,
                    };
                }
            ])

            .controller("ImageStreamDeleteCtrl", [
                "$scope",
                "$modalInstance",
                "dialogData",
                "kubeMethods",
                function($scope, $instance, dialogData, methods) {
                    angular.extend($scope, dialogData);

                    $scope.performDelete = function performDelete() {
                        return methods.delete($scope.stream);
                    };
                }
            ])

            .controller("ImageStreamModifyCtrl", [
                "$scope",
                "$modalInstance",
                "dialogData",
                "imageTagData",
                "kubeMethods",
                "filterService",
                "gettextCatalog",
                function($scope, $instance, dialogData, tagData, methods, filter, gettextCatalog) {
                    var stream = dialogData.stream || { };
                    var meta = stream.metadata || { };
                    var spec = stream.spec || { };
                    const _ = gettextCatalog.getString.bind(gettextCatalog);

                    var populate = "none";
                    if (spec.dockerImageRepository)
                        populate = "pull";
                    if (spec.tags)
                        populate = "tags";

                    var fields = {
                        name: meta.name || "",
                        project: meta.namespace || filter.namespace() || "",
                        populate: populate,
                        pull: spec.dockerImageRepository || "",
                        tags: tagData.parseSpec(spec),
                        insecure: hasInsecureTag(spec),
                    };

                    $scope.fields = fields;
                    $scope.labels = {
                        populate: {
                            none: _("Don't pull images automatically"),
                            pull: _("Sync all tags from a remote image repository"),
                            tags: _("Pull specific tags from another image repository"),
                        }
                    };

                    $scope.placeholder = _("eg: my-image-stream");

                    /* During creation we have a different label */
                    if (!dialogData.stream)
                        $scope.labels.populate.none = _("Create empty image stream");

                    function performModify() {
                        var data = {
                            apiVersion: "v1",
                            kind: "ImageStream",
                            metadata: { annotations:  { "openshift.io/image.dockerRepositoryCheck" : null } }
                        };

                        if (fields.populate == "pull")
                            data.spec = { dockerImageRepository: fields.pull.trim(), };
                        else if (fields.populate == "tags")
                            data.spec = tagData.buildSpec(fields.tags, data.spec, fields.insecure, fields.pull.trim());
                        else
                            data.spec = { dockerImageRepository: null, tags: null };

                        return methods.patch(stream, data);
                    }

                    function performCreate() {
                        var data = {
                            apiVersion: "v1",
                            kind: "ImageStream",
                            metadata: {
                                name: fields.name.trim(),
                                namespace: fields.project.trim(),
                            }
                        };

                        if (fields.populate == "pull")
                            data.spec = { dockerImageRepository: fields.pull.trim(), };
                        else if (fields.populate == "tags")
                            data.spec = tagData.buildSpec(fields.tags, data.spec, fields.insecure, fields.pull.trim());

                        return methods.check(data, {
                            "metadata.name": "#imagestream-modify-name",
                            "metadata.namespace": "#imagestream-modify-project",
                        }).then(function() {
                            return methods.create(data, fields.project);
                        });
                    }

                    function hasInsecureTag(spec) {
                        // loop through tags, check importPolicy.insecure boolean
                        // if one tag is insecure the intent is the imagestream is insecure
                        var insecure;
                        if (spec) {
                            for (var tag in spec.tags) {
                                if (spec.tags[tag].importPolicy.insecure) {
                                    insecure = spec.tags[tag].importPolicy.insecure;
                                    break;
                                }
                            }
                        }
                        return insecure;
                    }

                    $scope.performCreate = performCreate;
                    $scope.performModify = performModify;
                    $scope.hasInsecureTag = hasInsecureTag;

                    $scope.projects = filter.namespaces;
                    angular.extend($scope, dialogData);
                }
            ])

            .controller("ImageDeleteCtrl", [
                "$scope",
                "$modalInstance",
                "dialogData",
                "kubeMethods",
                function($scope, $instance, dialogData, methods) {
                    angular.extend($scope, dialogData);

                    $scope.performDelete = function performDelete() {
                        var name = $scope.stream.metadata.name + ":" + $scope.tag.tag;
                        return methods.delete("ImageStreamTag", name, $scope.stream.metadata.namespace);
                    };
                }
            ]);
}());
