/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import angular from 'angular';
import angularAnimate from 'angular-animate';
import uibModal from 'angular-ui-bootstrap/src/modal/index-nocss';
import template from './catalog-saver.template.html';
import modalTemplate from './catalog-saver.modal.template.html';
import jsYaml from 'js-yaml';
import brUtils from 'brooklyn-ui-utils/utils/general';

const MODULE_NAME = 'brooklyn.components.catalog-saver';
const TEMPLATE_URL = 'blueprint-composer/component/catalog-saver/index.html';
const TEMPLATE_MODAL_URL = 'blueprint-composer/component/catalog-saver/modal.html';

const REASONS = {
    new: 0,
    deploy: 1
};
const VIEWS = {
    form: 0,
    saved: 1
};
const TYPES = [
    {id: 'template', label: 'Application'},
    {id: 'entity', label: 'Entity'}
];

angular.module(MODULE_NAME, [angularAnimate, uibModal, brUtils])
    .directive('catalogSaver', ['$rootScope', '$uibModal', '$injector', '$filter', 'composerOverrides', 'blueprintService', saveToCatalogModalDirective])
    .directive('catalogVersion', ['$parse', catalogVersionDirective])
    .directive('composerBlueprintNameValidator', composerBlueprintNameValidatorDirective)
    .filter('bundlize', bundlizeProvider)
    .run(['$templateCache', templateCache]);

export default MODULE_NAME;

export function saveToCatalogModalDirective($rootScope, $uibModal, $injector, $filter, composerOverrides, blueprintService) {
    return {
        restrict: 'E',
        templateUrl: function (tElement, tAttrs) {
            return tAttrs.templateUrl || TEMPLATE_URL;
        },
        scope: {
            config: '=',
        },
        link: link
    };

    function link($scope, $element) {
        if (!$scope.config.original) {
            // original if provided contains the original metadata, e.g. for use if coming from a template and switching between template and non-template
            $scope.config.original = {}
        }
        $scope.isNewFromTemplate = () => ($scope.config.itemType !== 'template' && $scope.config.original.itemType === 'template');
        $scope.isUpdate = () => !$scope.isNewFromTemplate() && Object.keys($scope.config.original).length>0;
        $scope.buttonTextFn = () => $scope.config.label || ($scope.isUpdate() && ($scope.config.name || $scope.config.original.name || $scope.config.symbolicName || $scope.config.original.symbolicName)) || 'Add to catalog';
        $scope.buttonText = $scope.buttonTextFn(); 
        
        $scope.activateModal = () => {
            let entity = blueprintService.get();
            let metadata = blueprintService.entityHasMetadata(entity) ? blueprintService.getEntityMetadata(entity) : new Map();
              
            if (!$scope.config.itemType) {
                // This is the default item type
                $scope.config.itemType = 'application';
            }
            
            // Set various properties from the blueprint entity data if not already set
            if (!$scope.config.iconUrl && (entity.hasIcon() || metadata.has('iconUrl'))) {
                $scope.config.iconUrl = entity.icon || metadata.get('iconUrl');
            }
            if (!$scope.isNewFromTemplate()) {
                // (these should only be set if not making something new from a template, as the entity items will refer to the template)
                
                // the name and the ID can be set in the UI, 
                // or all can be inherited if root node is a known application type we are editting 
                // (normally in those cases $scope.config will already be set by caller, but maybe not always) 
                if (!$scope.config.name && entity.hasName()) {
                    $scope.config.name = entity.name;
                }
                if (!$scope.config.symbolicName && (entity.hasId() || metadata.has('id'))) {
                    $scope.config.symbolicName = entity.id || metadata.get('id');
                }
                if (!$scope.config.version && (entity.hasVersion() || metadata.has('version'))) {
                    $scope.config.version = entity.version || metadata.get('version');
                }
                if (!$scope.config.bundle) {
                    if ($scope.config.symbolicName) {
                        $scope.config.bundle = $scope.config.symbolicName;
                    }
                }
            }
            
            // Override this callback to update configuration data elsewhere
            $scope.config = (composerOverrides.updateCatalogConfig || ((config, $element) => config))($scope.config, $element);

            let modalInstance = $uibModal.open({
                templateUrl: TEMPLATE_MODAL_URL,
                size: 'save',
                controller: ['$scope', '$filter', 'blueprintService', 'paletteApi', 'brUtilsGeneral', CatalogItemModalController],
                scope: $scope,
            });
            
            // Promise is resolved when the modal is closed. We expect the modal to pass back the action to perform thereafter
            modalInstance.result.then(reason => {
                switch (reason) {
                    case REASONS.new:
                        $rootScope.$broadcast('blueprint.reset');
                        break;
                    case REASONS.deploy:
                        $rootScope.$broadcast('blueprint.deploy');
                        break;
                }
            });
        };
    }
}

export function CatalogItemModalController($scope, $filter, blueprintService, paletteApi, brUtilsGeneral) {
    $scope.REASONS = REASONS;
    $scope.VIEWS = VIEWS;
    $scope.TYPES = TYPES;
    $scope.state = {
        pattern: '[\\w\\.\\-\\_]+',
        view: VIEWS.form,
        saving: false,
        force: false,
    };

    $scope.getTitle = () => {
        switch ($scope.state.view) {
            case VIEWS.form:
                return $scope.isUpdate() ? `Update ${$scope.config.name || $scope.config.symbolicName || 'blueprint'}` : 'Add to catalog';
            case VIEWS.saved:
                return `${$scope.config.name || $scope.config.symbolicName || 'Blueprint'} ${$scope.isUpdate() ? 'updated' : 'saved'}`;
        }
    };
    $scope.title = $scope.getTitle();

    $scope.save = () => {
        $scope.state.saving = true;
        $scope.state.error = undefined;

        let bom = createBom();
        paletteApi.create(bom, {forceUpdate: $scope.state.force}).then((savedItem) => {
            if (!angular.isArray($scope.config.versions)) {
                $scope.config.versions = [];
            }
            $scope.config.versions.push($scope.config.version);
            $scope.state.view = VIEWS.saved;
        }).catch(error => {
            $scope.state.error = error.error.message;
        }).finally(() => {
            $scope.state.saving = false;
        });
    };

    function createBom() {
        let blueprint = blueprintService.getAsJson();

        let bundleBase = $scope.config.bundle || $scope.defaultBundle;
        let bundleId = $scope.config.symbolicName || $scope.defaultSymbolicName;
        if (!bundleBase || !bundleId) {
            throw "Either the display name must be set, or the bundle and symbolic name must be explicitly set";
        }
        
        let bomItem = {
            id: bundleId,
            itemType: $scope.config.itemType,
            item: blueprint
        };
        let bomCatalogYaml = {
            bundle: `catalog-bom-${bundleBase}`,
            version: $scope.config.version,
            items: [ bomItem ]
        };
        let bundleName = $scope.config.name || $scope.defaultName;
        if (brUtilsGeneral.isNonEmpty(bundleName)) {
            bomItem.name = bundleName;
        }
        if (brUtilsGeneral.isNonEmpty($scope.config.description)) {
            bomItem.description = $scope.config.description;
        }
        if (brUtilsGeneral.isNonEmpty($scope.config.iconUrl)) {
            bomItem.iconUrl = $scope.config.iconUrl;
        }
        
        return jsYaml.dump({ 'brooklyn.catalog': bomCatalogYaml });
    }

    let bundlize = $filter('bundlize');
    $scope.updateDefaults = (newName) => {
        $scope.defaultName = ($scope.config.itemType==='template' && $scope.config.original.name) || null;
        $scope.defaultSymbolicName = ($scope.config.itemType==='template' && $scope.config.original.symbolicName) || bundlize(newName) || null;
        $scope.defaultBundle = ($scope.config.itemType==='template' && $scope.config.original.bundle) || bundlize(newName) || null;
    };
    $scope.$watchGroup(['config.name', 'config.itemType', 'config.bundle', 'config.symbolicName'], (newVals) => {
        $scope.updateDefaults(newVals[0]);
        $scope.form.name.$validate();
        $scope.buttonText = $scope.buttonTextFn();
        $scope.title = $scope.getTitle();
    });
}

function composerBlueprintNameValidatorDirective() {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attr, ngModel) {
            ngModel.$validators.composerBlueprintNameValidator = function(modelValue, viewValue) {
                scope.updateDefaults(modelValue);
                if (!ngModel.$isEmpty(modelValue)) {
                    // anything set is valid
                    return true;
                }
                // if not set, we need a bundle and symbolic name
                if (scope.config.bundle && scope.config.symbolicName) {
                    return true;
                }
                // or if we have defaults for bundle and symbolic name we don't need this name
                if (scope.defaultBundle && scope.defaultSymbolicName) {
                    return true;
                }
                return false;
            }
        },
    };
}

export function catalogVersionDirective($parse) {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: link
    };

    function link(scope, elm, attr, ctrl) {
        if (!ctrl) {
            return;
        }

        let matches;
        let force;

        scope.$watch(attr.catalogVersion, value => {
            if (matches !== value) {
                matches = value;
                ctrl.$validate();
            }
        });
        scope.$watch(attr.catalogVersionForce, value => {
            if (force !== value) {
                force = value;
                ctrl.$validate();
            }
        });

        ctrl.$validators.exist = (modelValue, viewValue) => {
            return !angular.isDefined(matches) || ctrl.$isEmpty(viewValue) || viewValue.endsWith('SNAPSHOT') || force === true || matches.indexOf(viewValue) === -1;
        };        
    }
}

function templateCache($templateCache) {
    $templateCache.put(TEMPLATE_URL, template);
    $templateCache.put(TEMPLATE_MODAL_URL, modalTemplate);
}

function bundlizeProvider() { 
    return (input) => input && input.split(/[^a-zA-Z0-9]+/).filter(x => x).join('-').toLowerCase(); 
}
