/*
 * 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 CodeMirror from 'codemirror';
import 'codemirror/mode/yaml/yaml';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/indent-fold';
import 'codemirror/addon/fold/comment-fold';
import 'codemirror/addon/lint/lint';
import './addon/lint/lint-yaml-brooklyn';
import './addon/lint/lint-yaml-tab';
import 'codemirror/addon/hint/show-hint';
import './addon/hint/super-hint';
import './addon/hint/super-hint-yaml-schema';
import './addon/hint/super-hint-yaml-type';
import './addon/hint/super-hint-yaml-anchor';

const MODULE_NAME = 'brooklyn.component.yaml-editor';

angular.module(MODULE_NAME, [])
    .directive('brYamlEditor', ['$rootScope', 'brSnackbar', yamlEditorDirective]);

export default MODULE_NAME;

export function yamlEditorDirective($rootScope, brSnackbar) {
    return {
        restrict: 'E',
        scope: {
            type: '@',
            value: '=',
            onChange: '&'
        },
        link: link
    };

    function link($scope, $element) {
        $scope.type = $scope.type || 'brooklyn';

        // Init CodeMirror
        CodeMirror.extendMode('yaml', {
            fold: 'indent',
            type: $scope.type
        });

        let lint = (text, options, cm) => {
            let issues = cm.getHelpers(CodeMirror.Pos(0, 0), 'lint').reduce((issues, helper) => issues.concat(helper(text, options, cm)), []);

            $rootScope.$broadcast('yaml.lint', issues.some(issue => !issue.severity || issue.severity === 'error'), cm.getValue());

            return issues;
        };
        let hint = (cm, callback, options) => {
            let loadingState = createLoadingState();
            let removeLoadingState = () => {
                if (loadingState) {
                    loadingState.remove();
                }
                cm.off('cursorActivity', removeLoadingState);
            };

            cm.on('cursorActivity', removeLoadingState);
            cm.addWidget(cm.getCursor(), loadingState);

            let promises = cm.getHelpers(CodeMirror.Pos(0, 0), 'hint').reduce((promises, helper) => promises.concat(helper(cm, options)), []);

            Promise.all(promises).then(results => {
                removeLoadingState();

                callback(results.reduce((hints, result) => {
                    let list = hints.list.concat(result.list);
                    list.sort();
                    return Object.assign(hints, result, {list: list});
                }, {list: []}));
            }).catch(error => {
                removeLoadingState();

                $scope.$apply(() => {
                    brSnackbar.create(error.message);
                });

                callback({
                    list: [],
                    from: cm.getCursor(),
                    to: cm.getCursor()
                });
            });
        };
        hint.async = true;

        let createLoadingState = () => {
            let loadingState = document.createElement('ul');
            loadingState.className = 'CodeMirror-hints';
            let loading = loadingState.appendChild(document.createElement('li'));
            loading.className = 'CodeMirror-hint CodeMirror-hint-loading';
            loading.appendChild(document.createTextNode('Loading'));

            return loadingState;
        };

        $scope.cm = new CodeMirror($element[0], {
            value: $scope.value || '',
            styleActiveLine: true,
            matchBrackets: true,
            theme: 'eclipse',
            lineNumbers: true,
            lineWrapping: true,
            foldGutter: true,
            gutters: ['CodeMirror-linenumbers', 'CodeMirror-lint-markers', 'CodeMirror-foldgutter'],
            mode: {
                name: 'yaml',
                globalVars: true
            },
            lint: lint,
            extraKeys: {
                'Ctrl-Space': 'autocomplete',
                'Tab': cm => {
                    let wideSelection = cm.listSelections().reduce((acc, range) => {
                        if ((range.anchor.line !== range.head.line) || (range.anchor.ch !== range.head.ch)) {
                            return acc || true;
                        }
                        return acc;
                    }, false);

                    if (wideSelection) {
                        CodeMirror.commands.indentMore(cm);
                    } else {
                        cm.replaceSelection('  ', 'end');
                    }
                }
            },
            foldOptions: {
                widget: '{ ... }'
            }
        });
        $scope.cm.on('changes', (cm, change)=> {
            if ($scope.value !== cm.getValue()) {
                $scope.$apply(()=> {
                    $scope.value = cm.getValue();
                    let fn = $scope.onChange();
                    if (angular.isFunction(fn)) {
                        fn(cm);
                    }
                });
            }
        });
        $scope.cm.on('superhint-error', (error) => {
            $scope.$apply(()=> {
                brSnackbar.create(error.message);
            });
        });

        $scope.$watch('value', (newVal, oldVal)=> {
            $scope.cm.getDoc();
            $scope.cm.focus();
            if ($scope.cm.getValue() !== newVal && angular.isDefined(newVal)) {
                $scope.cm.setValue(newVal);
            }
            setTimeout(()=> {
                $scope.cm.focus();
                $scope.cm.refresh();
            });
        });
    }
}
