angular.module('cmdbDirectivesModule', [])
    .directive('cmdb', ['cmdbService', '$modal', '$sce', function (cmdbService, $modal, $sce) {
    return {
        templateUrl: MP_SITE_URL + '/directives/partials/cmdb.php',
        restrict: 'A',
        scope: {
            identifier: '=',
            ishub: '=',
            displayShowJson: '=',
            cmdbServiceUrl: '@'
        },

        link: function ($scope, element, attr, ngModel) {
            const arrayType = [].constructor.name;
            const objectType = {}.constructor.name;
            const stringType = "".constructor.name;

            if ($scope.cmdbServiceUrl) {
                cmdbService.setUrl($scope.cmdbServiceUrl);
            }
            $scope.maxNameLength = 255;
            $scope.maxValueLength = 16000;
            $scope.namePattern = '^[a-zA-Z0-9_.:]+$';
            $scope.modalWindow = $modal;
            $scope.totextarea = {};
            $scope.highlight = {};

            $scope.filtered = {
                variables: [],
                classes: []
            };

            $scope.editing = {
                variables: null,
                classes: null
            }
            $scope.pagination = {
                perPage: 3,
                variables: {
                    page: 1,
                    max: 1,
                    total: 0
                }, classes: {
                    page: 1,
                    max: 1,
                    total: 0
                }
            };
            $scope.new = {
                variables: null,
                classes: null
            };
            $scope.suggestions = {
                list: CmdbSuggestions,
                selected: {},
                opened: false
            };

            $scope.types = {"String": "String", "Array": "List", "Object": "Data container"};

            const getItemsFromApi = (callback = null, ...args) => {
                cmdbService.getItems($scope.identifier).then(function (response) {
                    let unsorted = response.data.value;
                    $scope.cmdbValue = {
                        variables: (unsorted && unsorted.hasOwnProperty('variables') && unsorted.variables != null) ?
                            sortObjectAlphabetically(unsorted.variables) :
                            {},
                        classes: (unsorted && unsorted.hasOwnProperty('classes') && unsorted.classes != null) ?
                            sortObjectAlphabetically(unsorted.classes) :
                            {},
                    };
                    callback !== null && callback(args);
                }).catch(function (error) {
                    notify.error(error);
                });
            }
            getItemsFromApi();

            $scope.addOpenedClass = function (event) {
                event.target.closest("div").classList += " opened";
                event.stopPropagation();
            }

            $scope.renderContent = function (content, type) {
                if (type == undefined) {
                    type = content.constructor.name;
                }

                if (content) {
                    switch(type) {
                        case objectType:
                            return JSON.stringify(content);
                            break;
                        case arrayType:
                            return Array.isArray(content) ? content.join(', ') : content;
                            break;
                    }
                }
                return content;
            }

            $scope.cancelEditing = function (index) {
                $scope.editing.variables = null;
                $scope.editing.classes = null;
            }

            $scope.inputFocused = function (event, property, parent) {
                const type = parent.type ||  parent[property].constructor.name;

                if ((type === arrayType || property == 'tags') && parent[property]) {
                    parent[property] = parent[property].join("\n");
                }

                if (type === objectType && property !== 'tags') {
                    parent[property] = JSON.stringify(parent[property]);
                }

                if (type === arrayType || type === objectType || property == 'tags') {
                    $scope.totextarea[property] = true;
                    $scope.totextarea.type = type;
                    setTimeout(() => event.target.nextElementSibling.focus(), 50)
                }
            }

            $scope.textAreaBlur = function (event, property, parent, form = null) {
                const type = parent.type || parent[property].constructor.name;
                if (parent.type === arrayType || property === 'tags') {
                    parent[property] = parent[property].length > 0 ? parent[property].split("\n").filter(Boolean) : [];
                }

                if (form !== null && (form.value.$invalid || form.inputValue.$invalid)) {
                    return;
                }

                if (parent.type === objectType && parent[property].length > 0 && property !== 'tags') {
                    parent[property] = JSON.parse(parent[property]);
                }

                $scope.totextarea = {};
                if (event.relatedTarget !== null && event.relatedTarget.type === 'submit') {
                    event.relatedTarget.click()
                }
            }

            $scope.editVariable = function (index) {
                const type = $scope.filtered.variables[index].type || $scope.filtered.variables[index].value.constructor.name;
                $scope.editing.variables = {
                    index: index,
                    var: angular.copy($scope.filtered.variables[index]),
                    type
                };

                if ($scope.editing.variables.type === stringType) {
                    $scope.editing.variables.var.value =  decodeHtml($scope.editing.variables.var.value);
                }
                $scope.editing.variables.var.comment = decodeHtml($scope.editing.variables.var.comment);
                $scope.editing.variables.var.tags = $scope.editing.variables.var.tags.map(tag => decodeHtml(tag))

                $(document).trigger('CMDB_EDIT_OPENED');
                $scope.highlight = {};
            }

            $scope.editClass = function (index) {
                $scope.editing.classes = angular.copy($scope.filtered.classes[index]);
                $scope.editing.classes.index = index;
                $scope.editing.classes.comment = decodeHtml($scope.editing.classes.comment)
                if (Array.isArray($scope.editing.classes.tags)) {
                    $scope.editing.classes.tags = $scope.editing.classes.tags.map(tag => decodeHtml(tag));
                }

                $(document).trigger('CMDB_EDIT_OPENED');
                $scope.highlight = {};
            }

            $scope.addItemAction = function (type) {
                $scope.new[type] = type === 'variables' ?
                    {name: "", value: "", type: 'String', tags: ['report'], comment: ""} :
                    {name: "", tags: ['report'], comment: ""};

                if (($scope.pagination[type].total % $scope.pagination.perPage) === 0) {
                    $scope.pagination[type].page = $scope.pagination[type].max + 1;
                } else {
                    $scope.pagination[type].page = $scope.pagination[type].max;
                }
                $(document).trigger('CMDB_EDIT_OPENED');
                $scope.highlight = {};
            }

            $scope.cancelAdding = function (type) {
                $scope.new[type] = null;
                $scope.pagination[type].page = $scope.pagination[type].max;
            }

            const decodeHtml = (text) => angular.element('<div />').html(text).text();

            const getPageByItem = (name, object) => Math.ceil((Object.keys(object).indexOf(name) + 1) / $scope.pagination.perPage);

            $scope.save = function (type, name, data, isNew = true, highlight = true) {
                const callback = () => {
                    getItemsFromApi((args) => {
                        $scope.pagination[type].page = getPageByItem(args[0], $scope.cmdbValue[type]);
                        if (highlight) {
                            $scope.highlight = {
                                name: args[0],
                                type: type
                            };
                        }
                    }, data.name);
                    $scope.cancelEditing();
                    $scope.cancelAdding(type);
                }

                if(isNew) {
                    cmdbService.createItem($scope.identifier, type, name, data).then(() => {
                        callback();
                    })
                } else {
                    cmdbService.updateItem($scope.identifier, type, name, data).then(() => {
                        callback();
                    });
                }
            }

            $scope.delete = function (type, name, identifier) {
                this.modalInstance = $scope.modalWindow.open({
                    templateUrl: 'deleteCMDB.html',
                    backdrop: 'static',
                    keyboard: true,
                    controller: function ($scope, $modalInstance, cmdbService) {
                        $scope.name = name;
                        $scope.removeItem = function () {
                            cmdbService.deleteItem(identifier, type, name).then(() => {
                                getItemsFromApi();
                                $scope.close();
                            });
                        }
                        $scope.close = function () {
                            $modalInstance.close('cancel');
                        };
                    }
                });
            };

            $scope.tooltippedName = function (name) {
                let result = {};
                result.Namespace = 'data';
                result.Bundle = 'variables';

                let matches;
                // parse namespace:bundle.variable
                if ((matches = name.match(/^([A-Za-z_0-9]+):([A-Za-z_0-9]+)\.([A-Za-z_0-9]+)$/)) != null) {
                    [, result.Namespace, result.Bundle, result.Variable] = matches
                    return result;
                }

                // parse bundle.variable
                if ((matches = name.match(/^([A-Za-z_0-9]+)\.([A-Za-z_0-9]+)$/)) != null) {
                    [, result.Bundle, result.Variable] = matches
                    return result;
                }

                // parse variable
                if ((matches = name.match(/^([A-Za-z_0-9]+)$/)) != null) {
                    [result.Variable] = matches
                    return result;
                }
            }

            $scope.tooltippedClassName = function (name) {
                let result = {"Namespace": 'data', "Class":  name};

                let matches;
                // parse namespace:class
                if ((matches = name.match(/^([A-Za-z_0-9]+):([A-Za-z_0-9]+)$/)) != null) {
                    [, result.Namespace, result.Class] = matches
                    return result;
                }

                return result;
            }

            $scope.addSuggestion = function (suggestion) {
                if (suggestion.hasOwnProperty('variables')) {
                    Object.keys(suggestion.variables).forEach(variableName => {
                        suggestion.variables[variableName].name = variableName;
                        $scope.save('variables', variableName, suggestion.variables[variableName]);
                    })
                } else if (suggestion.hasOwnProperty('classes')) {
                    Object.keys(suggestion.classes).forEach(className => {
                        suggestion.classes[className].name = className;
                        $scope.save('classes', className, suggestion.classes[className]);
                    })
                }
                $scope.suggestions.selected = {};
                $scope.suggestions.opened = false;
            };

            $scope.showJSON = function () {
                $scope.json = undefined;
                    common.globalSpinner.show();
                    cmdbService
                        .getJSONConfig($scope.identifier)
                        .then(
                            ({ data }) => {
                                common.globalSpinner.hide();
                                let json = $scope.json = (typeof data === "object" && data !== null) ? JSON.stringify(data, null, 4) : data;
                                this.modalInstance = $scope.modalWindow.open({
                                    templateUrl: 'showJsonCMDB.html',
                                    backdrop: 'static',
                                    keyboard: true,
                                    windowClass: 'json-config',
                                    controller: function ($scope, $modalInstance) {
                                        // bring json assigned above into this modal scope
                                        $scope.json = json;
                                        let editor;
                                        $scope.initEditor = function () {
                                            ace.config.set('modePath', '/scripts/node_modules/ace-builds/src-min-noconflict');
                                            ace.config.set('useWorker', false); // this disables syntax checking which we don't need in a read-only view
                                            editor = ace.edit('config', {
                                                readOnly: true,
                                                showPrintMargin: false,
                                                mode: "ace/mode/json",
                                                theme: `ace/theme/${isDarkModeEnabled() ? 'tomorrow_night' : 'textmate'}`
                                            });
                                        }
                
                                        $scope.copyToClipboard = () => common.copyTextToClipBoard($scope.json);
                
                                        $scope.$watch('json', function (value) {
                                            if (value === undefined) return;
                                            editor.setValue(value, -1);
                                        });
                
                                        $scope.close = function () {
                                            $modalInstance.close('cancel');
                                        };
                                    },
                                    resolve: {
                                        identifier: function () {
                                            return $scope.identifier;
                                        }
                                    }
                                });
                            })
                            .catch((error) => {
                                common.globalSpinner.hide();
                                if (error.status != 404) {
                                    notify.error("Problem getting data for this host: " + error.statusText);
                                }
                            });
            };

            // cast value to the new type when changed
            $scope.$watch('[new.variables.type, editing.variables.type]', (types) => {
                const [newVarType, editingVariablesType] = types;
                let type, obj;

                if (newVarType !== undefined) {
                    type = newVarType;
                    obj = $scope.new.variables;
                } else if (editingVariablesType !== undefined) {
                    type = editingVariablesType;
                    obj = $scope.editing.variables.var;
                    $scope.editing.variables.var.type = type;
                }

                switch (type) {
                    case arrayType:
                        obj.value = Array.isArray(obj.value) ? obj.value : [];
                        break;
                    case stringType:
                        obj.value = obj.value.constructor.name === stringType ? obj.value : '';
                        break;
                    case objectType:
                        obj.value = obj.value.constructor.name === objectType || obj.value.constructor.name === arrayType ? obj.value : {};
                        break;
                }
                $scope.totextarea = {};
            });

            $scope.$watch('cmdbValue', (data) => {
                if (data === undefined) {
                    return;
                }

                Object.keys(data).map(type => {
                    calculatePagination($scope.pagination[type], Object.keys(data[type]).length);
                    paginate(type);
                });

                processSuggestions();
            });


            const processSuggestions = function () {
                $scope.suggestions.list.forEach((suggestion, index) => {

                    // Remove items if suggestion is for hubs only and the current host is not a hub
                    if (!$scope.ishub && suggestion.only == 'hub') {
                        delete $scope.suggestions.list[index];
                    }

                    suggestion.disabled = false;
                    let suggestionTag = '';
                    let type, item;
                    if (suggestion.hasOwnProperty('variables')) {
                        type = 'variables';
                        item = suggestion.variables[Object.keys(suggestion.variables)[0]];
                    } else if (suggestion.hasOwnProperty('classes')) {
                        type = 'classes';
                        item = suggestion.classes[Object.keys(suggestion.classes)[0]];
                    }
                    suggestionTag = item.tags.find(value => /^suggestion-/.test(value));
                    suggestion.tooltip = suggestion.tooltip || item.comment; // Set tooltip from var/class comment if tooltip doesn't exist

                    // Disable already in use suggestions, based on suggestion-xxx tag.
                    for (const prop in $scope.cmdbValue[type]) {
                        const item = $scope.cmdbValue[type][prop];
                        if (item.hasOwnProperty('tags') && Array.isArray(item.tags)) {
                            if (item.tags.includes(suggestionTag)) {
                                suggestion.disabled = true;
                            }
                        }
                    }
                });

                $scope.suggestions.list = $scope.suggestions.list.filter(a => a != undefined); // remove delete op leftovers
            }

            const calculatePagination = function (pagination, totalItems) {
                pagination.total = totalItems;
                pagination.max = pagination.total == 0 ? 1 : Math.ceil(pagination.total / $scope.pagination.perPage);
                pagination.page = pagination.page > pagination.max ? pagination.max : pagination.page;
            }

            $scope.$watch('pagination.variables.page', (page) => {
                if (!page || $scope.cmdbValue == undefined) return;
                paginate('variables');
            });

            $scope.$watch('pagination.classes.page', (page) => {
                if (!page || $scope.cmdbValue == undefined) return;
                paginate('classes');
            });

            const paginate = (type) => {
                $scope.filtered[type] = Object.keys($scope.cmdbValue[type])
                    .slice(($scope.pagination[type].page - 1) * $scope.pagination.perPage, $scope.pagination[type].page * $scope.pagination.perPage)
                    .map(key => {
                        let item = Array.isArray($scope.cmdbValue[type][key]) ? {} : $scope.cmdbValue[type][key];
                        item.name = key;
                        return item;
                    });
                $(document).trigger('CMDB_PAGINATED');
            }

            $(document).click(function (e) {
                const suggestions = document.querySelector('.suggestions');
                if (suggestions && !suggestions.contains(e.target)) {
                    $scope.suggestions.opened = false;
                    $scope.$apply();
                }
            });

            // close actions dropdown on escape key press
            $(document).keyup(function (e) {
                if (e.key === "Escape") {
                    $scope.suggestions.opened = false;
                    $scope.$apply();
                }
            });
        }
    };
}])
    .directive('nameExists', function ($q, $parse, cmdbService) {
    return {
        restrict: 'A',
        require: 'ngModel',

        link: function (scope, elm, attr, model) {
            let timeout = null;
            model.$asyncValidators.nameExists = function () {
                let defer = $q.defer();
                const name = model.$viewValue;
                $parse(attr.ngModel).assign(scope, name);
                if (attr.initvalue != name) { // if initial name equals to name in the model then skip the validation
                    clearTimeout(timeout);
                    timeout = setTimeout(() => {
                        cmdbService.getItem(attr.identifier, attr.itemtype, name).then(
                            () => model.$setValidity('nameExists', false), // if name exists (200 status code), then name exists
                            () => model.$setValidity('nameExists', true) // in case when 400 status code name does not exist
                        )
                        defer.resolve;
                    }, 500) // delay between validation when typing
                } else {
                    model.$setValidity('nameExists', true)
                    defer.resolve;
                }
                return defer.promise;
            };
        }
    }
})
    .directive('validJson', function ($q, $parse) {
    return {
        restrict: 'A',
        require: 'ngModel',

        link: function (scope, elm, attr, model) {
            model.$asyncValidators.inValidJson = function () {
                let defer = $q.defer();
                const value = model.$viewValue;

                if (
                    (
                        model.$modelValue &&
                        (model.$modelValue.constructor.name == 'Object' || model.$modelValue.constructor.name == 'Array')
                    )
                    || attr.type !== 'Object' || value.length == 0
                ) {
                    defer.resolve();
                    return defer.promise;
                }

                try {
                    JSON.parse(value)
                    $parse(attr.ngModel).assign(scope, value);
                    defer.resolve();
                } catch {
                    defer.reject();
                }

                return defer.promise;
            };
        }
    }
})
    .directive('notCfeVariable', function ($q, $parse) {
    return {
        restrict: 'A',
        require: 'ngModel',

        link: function (scope, elm, attr, model) {
            model.$asyncValidators.CFEVariableDetected = function () {
                let defer = $q.defer();
                let value = model.$viewValue;
                const variableRegex = new RegExp('(\\$|\\@)(\\(.*\\)|{.*})', 'g');

                if (variableRegex.test(value)) {
                    defer.reject();
                } else {
                    defer.resolve();
                }

                return defer.promise;
            };
        }
    }
});
