text: /*
   * 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 angularSanitize from 'angular-sanitize';
  import {Dsl, DslParser, KIND} from '../util/model/dsl.model';
  import template from './dsl-editor.template.html';
  import brAutoFocus from 'brooklyn-ui-utils/autofocus/autofocus';
  import brUtils from 'brooklyn-ui-utils/utils/general';
  
  const MODULE_NAME = 'brooklyn.components.dsl-editor';
  const TEMPLATE_URL = 'blueprint-composer/component/dsl-editor/index.html';
  const DSL_KINDS = {
      ALL: {
          id: 'all',
          label: 'Config, sensor or entity',
      },
      CONFIG: {
          id: 'config',
          label: 'Config'
      },
      SENSOR: {
          id: 'sensor',
          label: 'Sensor'
      },
      ENTITY: {
          id: 'entity',
          label: 'Entity'
      },
      FORMAT_STRING: {
          id: 'formatString',
          label: 'Formatted string'
      },
  };
  
  angular.module(MODULE_NAME, [angularSanitize, brAutoFocus, brUtils])
      .directive('dslEditor', ['$rootScope', '$filter', '$log', 'brUtilsGeneral', 'blueprintService', dslEditorDirective])
      .run(['$templateCache', templateCache]);
  
  export default MODULE_NAME;
  
  export function dslEditorDirective($rootScope, $filter, $log, brUtilsGeneral, blueprintService) {
      return {
          restrict: 'E',
          templateUrl: function (tElement, tAttrs) {
              return tAttrs.templateUrl || TEMPLATE_URL;
          },
          scope: {
              definition: '=',
              entity: '=',
              dsl: '='
          },
          link: link
      };
  
      function link(scope) {
          scope.DSL_KINDS = DSL_KINDS;
  
          scope.kinds = Object.values(DSL_KINDS);
  
          scope.filters = [{
              id: 'blueprint',
              label: 'Anywhere on the blueprint',
              scope: 'Global'
          }].concat(getEntityItems(blueprintService.get()).map(item => {
              let attrs = [];
              if (item.entity === scope.entity) {
                  attrs.push('this');
              }
              if (!item.entity.hasParent()) {
                  attrs.push('root');
              }
              if (scope.entity.parent === item.entity) {
                  attrs.push('parent');
              }
  
              let name = item.name;
              if (attrs.length > 0) {
                  name += ` (${attrs.join(', ')})`;
              }
  
              return {
                  id: item.id,
                  label: name,
                  scope: 'On specific entity'
              };
          }));
  
          scope.orders = [{
              label: 'name',
              property: 'name'
          }, {
              label: 'entity',
              property: 'entity.type'
          }];
  
          scope.items = [].concat(
              getConfigItems(blueprintService.get(), scope.definition),
              getSensorItems(blueprintService.get()),
              getEntityItems(blueprintService.get(), scope.definition.type)
          );
  
          scope.state = {
              kind: scope.kinds[0],
              filter: scope.filters[0],
              orderBy: scope.orders[0],
              search: '',
              sensor: false,
              arguments: [],
              toggles: [],
              entityId: '',
          };
  
          if (scope.dsl) {
              let lastMethod = scope.dsl.getLastMethod();
              if (lastMethod.kind === KIND.METHOD && lastMethod.name === 'config') {
                  scope.state.kind = scope.kinds[1];
                  scope.state.search = lastMethod.params[0].name;
                  scope.state.item = scope.items.find(item => (item.type === DSL_KINDS.CONFIG && item.name === lastMethod.params[0].name));
              }
              if (lastMethod.kind === KIND.METHOD && ['attributeWhenReady', 'sensor'].includes(lastMethod.name)) {
                  scope.state.kind = scope.kinds[2];
                  scope.state.search = lastMethod.params[0].name;
                  scope.state.item = scope.items.find(item => (item.type === DSL_KINDS.SENSOR && item.name === lastMethod.params[0].name));
                  scope.state.sensor = lastMethod.name === 'sensor';
              }
              if (lastMethod.kind === KIND.UTILITY && lastMethod.name === 'formatString') {
                  scope.state.kind = scope.kinds[4];
                  scope.state.pattern = scope.dsl.params[0].name;
                  scope.state.arguments = Array.from(scope.dsl.params).splice(1).map(argument => {
                      return argument.kind === KIND.STRING ? argument.name : argument.toString();
                  });
              }
              let relatedEntity = scope.dsl.getRoot().relationships.find(entity => entity.id === scope.dsl.params[0].name);
              if (relatedEntity) {
                  scope.state.filter = scope.filters.find(filter => filter.id === relatedEntity._id);
              }
          }
  
          scope.$watch('state.pattern', (newValue, oldValue) => {
              if (!newValue || angular.equals(newValue, oldValue)) {
                  return;
              }
              scope.dsl.params.splice(0, 1, new Dsl(KIND.STRING, newValue));
          });
  
          scope.$watchCollection('state.arguments', (newValue, oldValue) => {
              if (!newValue || angular.equals(newValue, oldValue)) {
                  return;
              }
              newValue.forEach((argument, index) => {
                  let dsl;
                  try {
                      dsl = new DslParser().parseString(argument, scope.entity, blueprintService.get());
                      scope.dsl.params.splice(index + 1, 1, dsl);
                  } catch (ex) {
                      $log.debug(`Argument ${index} is not a DSL. Defaulting to string`, ex);
                      dsl = new Dsl(KIND.STRING, argument);
                  }
                  scope.dsl.params.splice(index + 1, 1, dsl);
              })
          });
  
          scope.isDsl = (index) => {
              return scope.dsl.params[index + 1] instanceof Dsl && scope.dsl.params[index + 1].kind !== KIND.STRING;
          };
  
          scope.predicate = (value, index, array) => {
              let predicates = [];
  
              let validTypes = scope.state.kind.id === DSL_KINDS.ALL.id ? Object.values(DSL_KINDS).filter(type => type !== DSL_KINDS.FORMAT_STRING) : [scope.state.kind];
              predicates.push(validTypes.map(type => type.id).includes(value.type.id));
  
              if (scope.state.filter.id !== 'blueprint') {
                  predicates.push(scope.state.filter.id === value.entity._id);
              }
  
              if (scope.state.search) {
                  let searchPredicate = value.name.toLowerCase().indexOf(scope.state.search.toLowerCase()) > -1;
                  if (value.description) {
                      searchPredicate |= value.description.toLowerCase().indexOf(scope.state.search.toLowerCase()) > -1;
                  }
                  if ([ DSL_KINDS.ENTITY.id, DSL_KINDS.ALL.id ].includes(scope.state.kind.id) ) {
                      // if searching for entity or config/sensors/entity, show everything.
                      // but searching just for a config or sensor doesn't show everything.
                      // (not sure that's the right semantics?)
                      searchPredicate |= value.entity.id && value.entity.id.toLowerCase().indexOf(scope.state.search.toLowerCase()) > -1;
                      searchPredicate |= value.entity.name && value.entity.name.toLowerCase().indexOf(scope.state.search.toLowerCase()) > -1;
                  }
                  predicates.push(searchPredicate);
              }
              return predicates.reduce((ret, predicate) => (ret && predicate), true);
          };
  
          scope.selectItem = (item, event) => {
              scope.state.item = item;
  
              event.preventDefault();
              event.stopPropagation();
          };
  
          scope.selectDsl = () => {
              scope.dsl = buildDsl();
  
              $rootScope.$broadcast(`${MODULE_NAME}.select`, {
                  dsl: scope.dsl,
                  definition: scope.definition
              });
          };
  
          scope.nestDsl = (index) => {
              scope.dsl = buildDsl();
  
              $rootScope.$broadcast(`${MODULE_NAME}.nest`, {
                  dsl: scope.dsl,
                  definition: scope.definition,
                  index: index
              });
          };
  
          scope.isDone = () => {
              if (scope.state.kind.id !== DSL_KINDS.FORMAT_STRING.id) {
                  return angular.isDefined(scope.state.item);
              } else {
                  return brUtilsGeneral.isNonEmpty(scope.state.pattern) && brUtilsGeneral.isNonEmpty(scope.state.arguments);
              }
          };
  
          function buildDsl() {
              let dsl;
              let funcDsl;
  
              if (scope.state.kind.id !== DSL_KINDS.FORMAT_STRING.id) {
                  let scopedDsl = getScopedDsl(scope.entity, scope.state.item.entity, scope.state);
  
                  switch (scope.state.item.type) {
                      case DSL_KINDS.CONFIG:
                          funcDsl = new Dsl(KIND.METHOD, 'config').param(new Dsl(KIND.STRING, scope.state.item.name));
                          scopedDsl.chain(funcDsl);
                          dsl = isSelfDsl(scopedDsl) ? funcDsl : scopedDsl;
                          break;
                      case DSL_KINDS.SENSOR:
                          funcDsl = new Dsl(KIND.METHOD, scope.state.sensor ? 'sensor' : 'attributeWhenReady').param(new Dsl(KIND.STRING, scope.state.item.name));
                          scopedDsl.chain(funcDsl);
                          dsl = isSelfDsl(scopedDsl) ? funcDsl : scopedDsl;
                          break;
                      case DSL_KINDS.ENTITY:
                          dsl = scopedDsl;
                          break;
                  }
              } else {
                  dsl = new Dsl(KIND.UTILITY, 'formatString').param(new Dsl(KIND.STRING, scope.state.pattern));
                  scope.state.arguments.forEach((arg, index) => {
                      dsl.param(scope.isDsl(index) ? scope.dsl.params[index + 1] : new Dsl(KIND.STRING, arg));
                  });
              }
  
              try {
                  dsl = new DslParser().parseString(dsl.toString(), scope.entity, blueprintService.get());
              } catch (ex) {
                  $log.debug(`Cannot get DSL relationship for DSL "${dsl}`, ex);
              }
  
              return dsl;
          }
      }
  
      function getConfigItems(entity, definition) {
          let config = entity.miscData.get('config').filter(config => config !== definition).map(config => {
              return {
                  id: config.name,
                  type: DSL_KINDS.CONFIG,
                  entity: entity,
                  name: config.name,
                  description: config.description
              };
          });
  
          let params = entity.miscData.get('parameters').filter(param => param !== definition).map(param => {
              return {
                  id: param.name,
                  type: DSL_KINDS.CONFIG,
                  entity: entity,
                  name: param.name,
                  description: param.description
              };
          });
          
          config = config.concat(params);
  
          config = Object.values(entity.getClusterMemberspecEntities()).reduce((acc, spec) => {
              return acc.concat(getConfigItems(spec, definition));
          }, config);
  
          return entity.children.reduce((acc, child) => {
              return acc.concat(getConfigItems(child, definition));
          }, config);
      }
  
      function getSensorItems(entity) {
          let sensors = entity.miscData.get('sensors').map(sensor => {
              return {
                  id: sensor.name,
                  type: DSL_KINDS.SENSOR,
                  entity: entity,
                  name: sensor.name,
                  description: sensor.description
              };
          });
  
          sensors = Object.values(entity.getClusterMemberspecEntities()).reduce((acc, spec) => {
              return acc.concat(getSensorItems(spec));
          }, sensors);
  
          return entity.children.reduce((acc, child) => {
              return acc.concat(getSensorItems(child));
          }, sensors);
      }
  
      function getEntityItems(entity, type) {
          let entities = [];
  
          entities.push({
              id: entity._id,
              type: DSL_KINDS.ENTITY,
              entity: entity,
              name: entity.miscData.get('typeName') || $filter('entityName')(entity) || 'New application',
              description: entity.description
          });
  
          entities = Object.values(entity.getClusterMemberspecEntities()).reduce((acc, spec) => {
              return acc.concat(getEntityItems(spec, type));
          }, entities);
  
          return entity.children.reduce((acc, child) => {
              return acc.concat(getEntityItems(child, type));
          }, entities);
      }
  
      function getScopedDsl(entity, targetEntity, state) {
          if (entity === targetEntity) {
              return new Dsl(KIND.TARGET, 'self');
          }
          if (entity.parent === targetEntity) {
              return new Dsl(KIND.TARGET, 'parent');
          }
  
          if (!targetEntity.hasId()) {
              if (brUtilsGeneral.isNonEmpty(state.entityId)) {
                  targetEntity.id = state.entityId;
              } else {
                  targetEntity.id = targetEntity.hasName() ?
                      targetEntity.name.replace(/\W/g, '-').toLowerCase() :
                      targetEntity._id;
              }
          }
  
          return new Dsl(KIND.METHOD, 'component').param(new Dsl(KIND.STRING, targetEntity.id));
      }
  
      function isSelfDsl(dsl) {
          return dsl && dsl.kind === KIND.TARGET && dsl.name === 'self';
      }
  }
  
  function templateCache($templateCache) {
      $templateCache.put(TEMPLATE_URL, template);
  }
  