text: 'use strict';
  
  const { mergeOptions, getTopLevelArtifactNameOf, getParentNameOf, getLastPartOf,
          getLastPartOfRef, getParentNamesOf } = require('../model/modelUtils');
  const { hasBoolAnnotation, isBuiltinType } = require('../model/csnUtils');
  const { compactModel, sortCsn } = require('../json/to-csn');
  const keywords = require('../base/keywords');
  const version = require('../../package.json').version;
  const alerts = require('../base/alerts');
  const { renderFunc } = require('./renderUtil');
  const DuplicateChecker = require('./DuplicateChecker');
  const { setProp, isBetaEnabled } = require('../base/model');
  const { checkCSNVersion } = require('../json/csnVersion');
  const { handleMessages } = require('../base/messages');
  const timetrace = require('../utils/timetrace');
  
  /**
   * @param {XSN.Model}   model
   * @param {CSN.Options} [options]
   */
  function toCdsSource(model, options){
    options = mergeOptions(model.options, options);
    let csn = compactModel(model);
    if (options.testMode)
      csn = sortCsn(csn, true);
  
    if (model.messages) {
      reclassifyMessages(model.messages);
      handleMessages(model);
      setProp(csn, 'messages', model.messages);
    }
  
    // Merge options (arguments first, then model options)
    return toCdsSourceCsn(csn, mergeOptions(csn.options, options));
  }
  
  /**
   * Check if any warnings (or info messages) were raised in earlier steps that
   * need to be reclassified - i.e. as errors.
   *
   * @param {object[]} messages
   */
  function reclassifyMessages(messages) {
    // Currently no message is reclassified.
    //
    // messages.map((message) => {
    //   switch (message.messageId) {
    //     case 'check-proper-type':
    //       message.severity = 'Error';
    //       break;
    //   }
    //   return message;
    // });
    return messages;
  }
  
  /**
   * Render the CSN model 'model' to CDS source text. One source is created per
   * top-level artifact. Return a dictionary of top-level artifacts
   * by their names, like this:
   * { "foo" : "using XY; context foo {...};",
   *   "bar::wiz" : "namespace bar::; entity wiz {...};"
   * }
   * If 'options.toHana' is set, render HANA-ish source dialect (currently
   * only affects translation of '$self.foo' in paths and ::-ish namespace declarations)
   * FIXME: This comment no longer tells the whole truth
   *
   * @param {CSN.Model} csn
   * @param {CSN.Options} [options]
  */
  function toCdsSourceCsn(csn, options) {
    timetrace.start('CDL rendering');
    let plainNames = options.forHana && options.forHana.names === 'plain';
    let hdbcdsNames = options.forHana && options.forHana.names === 'hdbcds';
    const cdlMode = !options.forHana;
    const {artifactRef} = require('../model/csnRefs').csnRefs(csn);
  
    // Skip compactModel if already using CSN
    //const csn = cloneCsn(model);
  
    const { signal, warning, error } = alerts(csn, options);
  
    checkCSNVersion(csn, options);
  
    let result = Object.create(null);
  
    // Create artificial namespace objects, so that each artifact has parents up to top-level.
    // FIXME: This should actually only be necessary for toHana (because that wants hierarchical
    // rendering). For CDL, we could just render each artifact top-level, with a dotted name.
    for (let artifactName in csn.definitions) {
      for (let parentName of getParentNamesOf(artifactName)) {
        if (!csn.definitions[parentName]) {
          csn.definitions[parentName] = {
            kind : 'namespace',
          };
        }
      }
    }
  
    let globalDuplicateChecker;
    if(plainNames) // enable duplicates check only for plain mode
      globalDuplicateChecker =  new DuplicateChecker(); // registry for all artifact names and element names
  
    // Render each top-level artifact on its own
    for (let artifactName in getTopLevelArtifacts()) {
      // This environment is passed down the call hierarchy, for dealing with
      // indentation and name resolution issues
      let env = createEnv();
      let sourceStr = renderArtifact(artifactName, csn.definitions[artifactName], env); // Must come first because it populates 'env.topLevelAliases'
      if (sourceStr !== '') {
        result[plainNames ? artifactName.replace(/\./g, '_').toUpperCase() : artifactName]
          = `${options.testMode ? '' : `// generated by cds-compiler version ${version} \n`}`
            + renderNamespaceDeclaration(artifactName, env) + renderUsings(artifactName, env) + sourceStr;
      }
    }
  
    globalDuplicateChecker && globalDuplicateChecker.check(signal, error); // perform duplicates check
  
    // If there are unapplied 'extend' and 'annotate' statements, render them separately
    // FIXME: Clarify if we should also do this for HANA (probably not?)
    if (!options.toHana && csn.extensions) {
      let env = createEnv();
      let sourceStr = renderUnappliedExtensions(csn.extensions, env);
      result.unappliedExtensions = renderUsings('', env) + sourceStr;
    }
  
    // Throw exception in case of errors
    handleMessages(csn, options);
    timetrace.stop();
    return result;
  
    // Render unapplied 'extend' and 'annotate' statements from the 'extensions array'
    function renderUnappliedExtensions(extensions, env) {
      return extensions.map(ext => {
        // Top-level annotations of the artifact
        let result = renderAnnotationAssignments(ext, env);
        result += `annotate ${ext.annotate}`;
        // Element extensions and annotations (possibly nested)
        if (ext.elements) {
          result += renderElementExtensions(ext.elements, env);
        }
        // Action annotations
        if (ext.actions) {
          result += ' actions {\n';
          let childEnv = increaseIndent(env);
          for (let name in ext.actions) {
            let action = ext.actions[name];
            result += renderAnnotationAssignments(action, childEnv) + childEnv.indent + quoteId(name);
            // Action parameter annotations
            if (action.params) {
              result += '(\n';
              let grandChildEnv = increaseIndent(childEnv);
              let paramAnnotations = []
              for (let paramName in action.params) {
                paramAnnotations.push(renderAnnotationAssignments(action.params[paramName], grandChildEnv) + grandChildEnv.indent + quoteId(paramName));
              }
              result += paramAnnotations.join(',\n') + '\n' + childEnv.indent + ')'
            }
            result += ';\n'
          }
          result += env.indent + '}'
        }
        result += ';';
        return result;
      }
      ).join('\n');
    }
  
    // Render the elements-specific part of an 'extend' or 'annotate' statement for an element dictionary
    // 'elements' (assuming that the surrounding parent has just been rendered, without trailing newline).
    // Return the resulting source string, ending without a trailing newline, too.
    function renderElementExtensions(elements, env) {
      let result = ' {\n';
      let childEnv = increaseIndent(env);
      for (let name in elements) {
        let elem = elements[name];
        result += renderAnnotationAssignments(elem, childEnv) + childEnv.indent + quoteId(name);
        if (elem.elements) {
          result += renderElementExtensions(elem.elements, childEnv);
        }
        result += ';\n'
      }
      result += env.indent + '}'
      return result;
    }
  
    // Render an artifact. Return the resulting source string.
    function renderArtifact(artifactName, art, env) {
      // FIXME: Correctly build the paths during runtime to give better locations
      env.path = ['definitions', artifactName];
      // Ignore whole artifacts if toHana says so
      if (art._ignore || options.toHana && hasBoolAnnotation(art, '@cds.persistence.exists', true)) {
        return '';
      }
      switch (art.kind) {
        case 'entity':
        case 'view':
          // FIXME: For HANA CDS, we need to replace $self at the beginning of paths in association ON-condition
          // by the full name of the artifact we are rendering (should actually be done by forHana, but that is
          // somewhat difficult because this kind of absolute path is quite unusual). In order not to have to pass
          // the current artifact name down through the stack to renderExpr, we just put it into the env.
          env.currentArtifactName = artifactName;
          if (art.query) {
            return renderView(artifactName, art, env);
          } else {
            return renderEntity(artifactName, art, env);
          }
        case 'context':
        case 'service':
          return renderContext(artifactName, art, env);
        case 'namespace':
          return renderNamespace(artifactName, art, env);
        case 'type':
        case 'annotation':
          return renderTypeOrAnnotation(artifactName, art, env);
        case 'action':
        case 'function':
          return renderActionOrFunction(artifactName, art, env);
        case 'event':
          return renderEventIfCDLMode(artifactName, art, env);
        default:
          throw new Error('Unknown artifact kind: ' + art.kind);
      }
    }
  
    // eslint-disable-next-line consistent-return
    function renderEventIfCDLMode(artifactName, art, env){
      if(cdlMode){
        let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
        let childEnv = increaseIndent(env);
        let normalizedArtifactName = renderArtifactName(artifactName, env);
        result += env.indent + 'event ' + normalizedArtifactName;
        if (art.includes) {
          // Includes are never flattened (don't exist in HANA)
          result += ' : ' + art.includes.map(name => renderAbsoluteNameWithQuotes(name, env)).join(', ');
        }
        if (art.query) {
          result += ' : ';
          result += renderQuery(art.query, true, 'projection', env,
                                ['definitions',artifactName,'query']);
          result += ';\n';
        } else if (art.type) {
          // Derived type or annotation with non-anonymous type
          result += ' : ' + renderTypeReference(art, env) + ';\n';
        }
        else if (art.elements) {
          result += ' {\n';
          for (let name in art.elements) {
            result += renderElement(name, art.elements[name], childEnv);
          }
          result += env.indent + '}';
        }
        return result;
      } else {
        return '';
      }
    }
  
    // Return a dictionary with the direct sub-artifacts of the artifact with name 'artifactName' in the csn
    function getSubArtifacts(artifactName) {
      let prefix = artifactName + '.';
      let result = Object.create(null);
      for (let name in csn.definitions) {
        // We have a direct child if its name starts with prefix and contains no more dots
        if (name.startsWith(prefix) && !name.substring(prefix.length).includes('.')) {
          result[getLastPartOf(name)] = csn.definitions[name];
        }
      }
      return result;
    }
  /* FIXME: Not yet required
    // Returns the artifact or element that constitutes the final type of
    // construct 'node', i.e. the object in which we would find type properties for
    // 'node'. Note that this may well be 'node' itself.
    function getFinalTypeOf(node) {
      if (node && node.type) {
        if (isBuiltinType(node.type)) {
          return node;
        }
        return getFinalTypeOf(node.type);
      }
      return node;
    }
  
    // Resolve path array 'ref' against artifact 'base' (or against 'csn.definitions'
    // if no 'base' given).
    // Return the resulting artifact or element (or 'undefined' if not found).
    function resolveRef(ref, base) {
      let result = base;
      for (let i = 0; i < ref.length; i++) {
        let pathStep = ref[i].id || ref[i];
        // Only first path step may be looked up in 'definitions'
        if (i === 0 && !base) {
          result = csn.definitions[pathStep];
          continue;
        }
        // Structured type
        else if (result && result.elements) {
          result = getFinalTypeOf(result.elements[pathStep]);
        }
        // Association
        else if (result && result.target) {
          result = resolveRef([pathStep], csn.definitions[result.target]);
        }
        // Not resolvable
        else {
          return undefined;
        }
      }
      return result;
    }
  */
  
    // Render a context or service. Return the resulting source string.
    function renderContext(artifactName, art, env) {
      let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
      let childEnv = increaseIndent(env);
      result += env.indent + (art.abstract ? 'abstract ' : '') + art.kind + ' ' + env.namePrefix + quoteId(getLastPartOf(artifactName));
      if (art.includes) {
        // Includes are never flattened (don't exist in HANA)
        result += ' : ' + art.includes.map(name => renderAbsoluteNameWithQuotes(name, env)).join(', ');
      }
      result += ' {\n';
      let subArtifacts = getSubArtifacts(artifactName);
      for (let name in subArtifacts) {
        result += renderArtifact(artifactName + '.' + name, subArtifacts[name], childEnv);
      }
      return result + env.indent + '};\n';
    }
  
    // Render a namespace. Return the resulting source string.
    function renderNamespace(artifactName, art, env) {
      // We currently do not render anything for a namespace, we just append its id to
      // the environment's current name prefix and descend into its children
      let result = renderDocComment(art, env);
      let childEnv = addNamePrefix(env, getLastPartOf(artifactName));
      let subArtifacts = getSubArtifacts(artifactName);
      for (let name in subArtifacts) {
        result += renderArtifact(artifactName + '.' + name, subArtifacts[name], childEnv);
      }
      return result;
    }
  
    // Render a (non-projection, non-view) entity. Return the resulting source string.
    function renderEntity(artifactName, art, env) {
      let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
      let childEnv = increaseIndent(env);
      let normalizedArtifactName = renderArtifactName(artifactName, env);
      globalDuplicateChecker && globalDuplicateChecker.addArtifact(normalizedArtifactName, art && art.$location, artifactName);
      result += env.indent + (art.abstract ? 'abstract ' : '') + 'entity ' + normalizedArtifactName;
      let parameters = Object.keys(art.params || []).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n');
      result += (parameters === '') ? '' : ' (\n' + parameters + '\n' + env.indent + ')';
      if (art.includes) {
        // Includes are never flattened (don't exist in HANA)
        result += ' : ' + art.includes.map(name => renderAbsoluteNameWithQuotes(name, env)).join(', ');
      }
      result += ' {\n';
      let duplicateChecker = new DuplicateChecker(); // registry for all artifact names and element names
      duplicateChecker.addArtifact(artifactName, art && art.$location, artifactName)
      for (let name in art.elements) {
        result += renderElement(name, art.elements[name], childEnv, duplicateChecker);
      }
      duplicateChecker.check(signal, error);
      result += env.indent + '}';
      result += renderActionsAndFunctions(art, env) + renderTechnicalConfiguration(art.technicalConfig, env) + ';\n';
      return result;
    }
  
    // Render the 'technical configuration { ... }' section 'tc' of an entity.
    // Return the resulting source string.
    function renderTechnicalConfiguration(tc, env) {
      let result = renderDocComment(tc, env);
      let childEnv = increaseIndent(env);
  
      if (!tc) {
        return result;
      }
  
      // FIXME: How to deal with non-HANA technical configurations? We should probably just iterate all entries
      // in 'tc' that we find and render them all (is it syntactically allowed yet to have more than one?)
      tc = tc.hana;
      if (!tc) {
        throw new Error('Expecting a HANA technical configuration');
      }
      result += '\n' + env.indent + 'technical ' + (tc.calculated ? '' : 'hana ') + 'configuration {\n';
  
      // Store type (must be separate because SQL wants it between 'CREATE' and 'TABLE')
      if (tc.storeType) {
        result += `${tc.storeType} store;\n`;
      }
      // Fixed parts belonging to the table (includes migration, unload prio, extended storage,
      // auto merge, partitioning, ...)
      if (tc.tableSuffix) {
        // Unlike SQL, CDL and HANA CDS require a semicolon after each table-suffix part
        // (e.g. `migration enabled; row store; ...`). In order to keep both
        // the simplicity of "the whole bandwurm is just one expression that can be
        // rendered to SQL without further knowledge" and at the same time telling
        // CDS about the boundaries, the compactor has put each part into its own `xpr`
        // object. Semantically equivalent because a "trivial" SQL renderer would just
        // concatenate them.
        for (let xpr of tc.tableSuffix) {
          result += childEnv.indent + renderExpr(xpr, childEnv) + ';\n';
        }
      }
  
      // Indices and full-text indices
      for (let idxName in tc.indexes || {}) {
        if (tc.indexes[idxName][0] instanceof Array) {
          // FIXME: Should we allow multiple indices with the same name at all?
          for (let index of tc.indexes[idxName]) {
            result += childEnv.indent + renderExpr(index, childEnv) + ';\n';
          }
        }
        else {
          result += childEnv.indent + renderExpr(tc.indexes[idxName], childEnv) + ';\n';
        }
      }
      // Fuzzy search indices
      for (let columnName in tc.fzindexes || {}) {
        if (tc.fzindexes[columnName][0] instanceof Array) {
          // FIXME: Should we allow multiple fuzzy search indices on the same column at all?
          // And if not, why do we wrap this into an array?
          for (let index of tc.fzindexes[columnName]) {
            result += childEnv.indent + renderExpr(fixFuzzyIndex(index, columnName), childEnv) + ';\n';
          }
        }
        else {
          result += childEnv.indent + renderExpr(fixFuzzyIndex(tc.fzindexes[columnName], columnName), childEnv) + ';\n';
        }
      }
      result += env.indent + '}';
      return result;
  
      // Fuzzy indices are stored in compact CSN as they would appear in SQL after the column name,
      // i.e. the whole line in SQL looks somewhat like this:
      //   s nvarchar(10) FUZZY SEARCH INDEX ON FUZZY SEARCH MODE 'ALPHANUM'
      // But in CDL, we don't write fuzzy search indices together with the table column, so we need
      // to insert the name of the column after 'ON' in CDS syntax, making it look like this:
      //   fuzzy search mode on (s) search mode 'ALPHANUM'
      // This function expects an array with the original expression and returns an array with the modified expression
      function fixFuzzyIndex(fuzzyIndex, columnName) {
        return fuzzyIndex.map(token => (token === 'on' ? { xpr: ['on', '(', { ref: columnName.split('.') }, ')'] } : token));
      }
    }
  
    // Render an element (of an entity, type or annotation, not a projection or view).
    // Return the resulting source string.
    function renderElement(elementName, elm, env, duplicateChecker) {
      // Ignore if toHana says so
      if (options.toHana && elm.virtual)
        elm._ignore = true;
      if (elm._ignore) {
        return '';
      }
      // Special handling for HANA CDS: Must omit the ':' before anonymous structured types (for historical reasons)
      let omitColon = (options.forHana && !elm.type && elm.elements);
      let result = renderDocComment(elm, env) + renderAnnotationAssignments(elm, env);
      const quotedElementName = quoteOrUppercaseId(elementName, elm.$location);
      duplicateChecker && duplicateChecker.addElement(quotedElementName, elm && elm.$location, elementName);
      result += env.indent + (elm.virtual ? 'virtual ' : '')
                           + (elm.key ? 'key ' : '')
                           + ((elm.masked && !elm._ignoreMasked)? 'masked ' : '')
                           + quotedElementName + (omitColon ? ' ' : ' : ')
                           + renderTypeReference(elm, env, undefined, true)
                           + renderNullability(elm);
      if (elm.default) {
        result += ' default ' + renderExpr(elm.default, env);
      }
      return result + ';\n';
    }
  
    // Return the SELECT of the leading query of query 'query'
    function leadingQuerySelect(query) {
      if (query.SELECT) {
        return query.SELECT;
      }
      // Sanity checks
      if (!query.SET || !query.SET.args || !query.SET.args[0]) {
        throw new Error(`Expecting set with args in query: ${JSON.stringify(query)}`);
      }
      return leadingQuerySelect(query.SET.args[0]);
    }
  
    // Render a query's actions and functions (if any) separately as extend-statements, so that actions
    // work not only for projections but also for views, which have no syntax (yet) to directly specify
    // actions and functions inline.
    // Return the resulting 'extend' statement or '' if no actions or functions
    // FIXME: Simplify once we have such a syntax
    function renderQueryActionsAndFunctions(artifactName, art, env) {
      let result = renderDocComment(art, env) + renderActionsAndFunctions(art, env);
      // Even if we have seen actions/functions, they might all have been ignored
      if (result !== '') {
        result = `${env.indent}extend entity ${artifactName} with${result};`;
      }
      return result;
    }
  
    // Render annotations that were extended to a query element of a view or projection (they only
    // appear in the view's 'elements', not in their 'columns', because the element itself may not
    // even be in 'columns', e.g. if it was expanded from a '*'). Return the resulting 'annotate'
    // statement or an empty string if none required.
    function renderQueryElementAnnotations(artifactName, art, env) {
      // If we are rendering for HANA CDS, never render any annotation (the only one we haven't already
      // stripped when we come here is '@cds.persistence.name', which is meant to stay, but only in the CSN).
      if (options.toHana) {
        return '';
      }
      // For preparation, create a map from element names to column objects
      let columnMap = Object.create(null);
      let select = leadingQuerySelect(art.query);
      for (let col of select.columns||['*']) {
        // Ignore '*'
        if (col === '*') {
          continue;
        }
        // Column must have an alias or be a path - take last part of that as element name
        columnMap[col.as || col.func || getLastPartOfRef(col.ref)] = col;
      }
      // Now iterate elements - render an annotation if it is different from the column's
      let childEnv = increaseIndent(env);
      let result = renderDocComment(art, env);
      for (let elemName in art.elements) {
        let elemAnnotations = '';
        let elem = art.elements[elemName];
        for (let name in elem) {
          if (!name.startsWith('@')) {
            continue;
          }
          let annotationValue = renderAnnotationValue(elem[name], childEnv);
          // Skip annotation if column has the same
          if (columnMap[elemName] && columnMap[elemName][name]
              && renderAnnotationValue(columnMap[elemName][name], childEnv) == annotationValue) {
            continue;
          }
          // Annotation names are never flattened
          elemAnnotations += `${childEnv.indent}${'@' + renderAbsoluteNameWithQuotes(name.substring(1), env)} : ${annotationValue}\n`;
        }
        if (elemAnnotations !== '') {
          result += `${elemAnnotations}${childEnv.indent}${elemName};\n`;
        }
      }
      if (result !== '') {
        result = `${env.indent}annotate ${renderArtifactName(artifactName, env)} with {\n${result}${env.indent}};\n`;
      }
      return result;
    }
  
    // Render the source of a query, which may be a path reference, possibly with an alias,
    // or a subselect, or a join operation, as seen from artifact 'art'.
    // Returns the source as a string.
    function renderViewSource(source, env) {
      // Sub-SELECT
      if (source.SELECT || source.SET) {
        let result = `(${renderDocComment(source, env)}${renderQuery(source, false, 'view', increaseIndent(env))})`;
        if (source.as) {
          result += ` as ${quoteOrUppercaseId(source.as)}`;
        }
        return result;
      }
      // JOIN
      else if (source.join) {
        // One join operation, possibly with ON-condition
        let result = `${renderDocComment(source, env)}${renderViewSource(source.args[0], env)}`;
        for (let i = 1; i < source.args.length; i++) {
          result = `(${result} ${source.join} `;
          if(!options.forHana) // HANA CDS doesn't understand join cardinality
            result += renderJoinCardinality(source.cardinality);
          result += `join ${renderViewSource(source.args[i], env)}`;
          if (source.on) {
            result += ` on ${renderExpr(source.on, env)}`;
          }
          result += ')';
        }
        return result;
      }
      // Ordinary path, possibly with an alias
      else {
        return renderAbsolutePathWithAlias(source, env, cdlMode);
      }
    }
  
    function renderJoinCardinality(card) {
      let result = '';
      if(card) {
        if(card.srcmin && card.srcmin === 1)
          result += 'exact ';
        result += card.src && card.src === 1 ? 'one ' : 'many ';
        result += 'to ';
        if(card.min && card.min === 1)
          result += 'exact ';
        if(card.max)
          result += (card.max === 1) ? 'one ' : 'many ';
      }
      return result;
    }
  
    /**
     * Render a path that starts with an absolute name (as used e.g. for the source of a query),
     * with plain or quoted names, depending on options. Expects an object 'path' that has a 'ref'.
     * Returns the name as a string.
     *
     * @param {object} path
     * @param {object} env
     * @param {boolean} joinWithColon
     * @return {string}
     */
    function renderAbsolutePath(path, env, joinWithColon) {
      // Sanity checks
      if (!path.ref) {
        throw new Error('Expecting ref in path: ' + JSON.stringify(path));
      }
      // Determine the absolute name of the first artifact on the path (before any associations or element traversals)
      let firstArtifactName = path.ref[0].id || path.ref[0];
  
      let result = renderDocComment(path, env);
      // Render the first path step (absolute name, with different quoting/naming ..)
      if (plainNames) {
        result += renderAbsoluteNamePlain(firstArtifactName, env);
      } else {
        result += renderAbsoluteNameWithQuotes(firstArtifactName, env);
      }
      // Even the first step might have parameters and/or a filter
      if (path.ref[0].args) {
        result += `(${renderArgs(path.ref[0].args, ':', env)})`;
      }
      if (path.ref[0].where) {
        result += `[${path.ref[0].cardinality ? (path.ref[0].cardinality.max + ': ') : ''}${renderExpr(path.ref[0].where, env)}]`;
      }
      // Add any path steps (possibly with parameters and filters) that may follow after that
      if (path.ref.length > 1) {
        result += `${joinWithColon ? ':' : '.'}${renderExpr({ref: path.ref.slice(1)}, env)}`;
      }
      return result;
    }
  
    /**
     * Render a path that starts with an absolute name (as used for the source of a query),
     * possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a
     * 'ref' and (in case of an alias) an 'as'. If necessary, an artificial alias
     * is created to the original implicit name.
     * Returns the name and alias as a string.
     *
     * @param {object} path
     * @param {object} env
     * @param {boolean} renderColon Render as explicit artifact reference with a :
     * @return {string}
     */
    function renderAbsolutePathWithAlias(path, env, renderColon) {
      let result = renderAbsolutePath(path, env, renderColon);
      // Take care of aliases
      let implicitAlias = getLastPartOfRef(path.ref);
      if (path.as) {
        // Source had an alias - render it
        result += ' as ' + quoteOrUppercaseId(path.as);
      }
      else if (getLastPartOf(result) != quoteOrUppercaseId(implicitAlias)) {
        // Render an artificial alias if the result would produce a different one
        result += ' as ' + quoteOrUppercaseId(implicitAlias);
      }
      return result;
    }
  
    // Render a single view or projection column 'col', as it occurs in a select list or
    // projection list within 'art', possibly with annotations.
    // Return the resulting source string (no trailing LF).
    function renderViewColumn(col, env) {
      // Ignore if toHana says so
      // FIXME: This probably needs to change ...
      if (col._ignore) {
        return '';
      }
      // Annotations and column
      let result = renderDocComment(col, env) + renderAnnotationAssignments(col, env);
  
      const leaf = col.as || col.ref && col.ref[col.ref.length-1];
      const element = env._artifact.elements[leaf];
      // Render 'null as <alias>' only for database and if element is virtual
  
      if(options.forHana && element && element.virtual) {
        const renderVirtual = isBetaEnabled(options, 'dontRenderVirtualElements') ? !!options.forHana.renderVirtualElements : true;
        if(renderVirtual)
          result += env.indent + 'null as ' + quoteOrUppercaseId(leaf);
      } else {
        result += env.indent;
        // only if column is virtual, keyword virtual was present in the source text
        if(col.virtual)
          result += 'virtual ';
        // If key is explicitly set in a non-leading query, issue an error.
        if(col.key && env.skipKeys){
          signal(error`KEY must only be added in the leading query`, env.path);
        }
        const key = (!env.skipKeys && (col.key || (options.forHana && element && element.key)) ? 'key ' : '');
        result += key + renderExpr(col, env, true);
        let alias = col.as;
        // HANA requires an alias for 'key' columns just for syntactical reasons
        // FIXME: This will not complain for non-refs (but that should be checked in forHana)
        // Explicit or implicit alias?
        // Shouldn't we simply generate an alias all the time?
        if (options.forHana && (key || col.cast) && !alias) {
          alias = leaf;
        }
        if (alias) {
          result += ' as ' + quoteOrUppercaseId(alias);
        }
        // Explicit type provided for the view element?
        if (col.cast) {
          // Special case: Explicit association type is actually a redirect
          if (col.cast.target) {
            // Redirections are never flattened (don't exist in HANA)
            result += ' : redirected to ' + renderAbsoluteNameWithQuotes(col.cast.target, env);
          } else {
            result += ' : ' + renderTypeReference(col.cast, env, true);
          }
        }
      }
      return result;
    }
  
    /**
     * Render .doc properties as comments in CDL
     *
     * @param {object} obj Object to render for
     * @param {object} env Env - for indent
     * @returns {String}
     */
    function renderDocComment(obj, env){
      if(!cdlMode || !(obj && obj.doc)) return '';
      else return `\n${env.indent}/**\n${obj.doc.split('\n').map(line => `${env.indent} * ${line}`).join('\n')}\n${env.indent} */\n`;
    }
  
    // Render a view. If '$syntax' is set (to 'projection', 'view', 'entity'),
    // the view query is rendered in the requested syntax style, otherwise it
    // is rendered as a view.
    function renderView(artifactName, art, env) {
      let syntax = art.$syntax || 'view';
      let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
      result += `${env.indent}${art.abstract ? 'abstract ' : ''}${syntax === 'projection' ? 'entity' : syntax} ${renderArtifactName(artifactName, env)}`;
      if (art.params) {
        let childEnv = increaseIndent(env);
        let parameters = Object.keys(art.params).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n');
        // HANA only understands the 'with parameters' syntax'
        if (options.forHana) {
          result += ` with parameters\n${parameters}\n${env.indent}as `;
        }
        // Otherwise prefer simple parentheses
        else {
          result += `(\n${parameters}\n${env.indent}) as `;
        }
      }
      else {
        result += ' as ';
      }
      env._artifact = art;
      result += renderQuery(art.query, true, syntax, env, ['definitions',artifactName,'query']);
      result += ';\n';
      result += renderQueryElementAnnotations(artifactName, art, env);
      result += renderQueryActionsAndFunctions(artifactName, art, env);
      return result;
    }
  
    // Render a query 'query', i.e. a select statement with where-condition etc.
    // If 'isLeadingQuery' is true, mixins, actions and functions of 'art' are
    // also rendered into the query. Use 'syntax'style ('projection', 'view',
    // or 'entity')
    function renderQuery(query, isLeadingQuery, syntax, env, path=[]) {
      let result = renderDocComment(query, env);
      env.skipKeys = !isLeadingQuery;
      // Set operator, like UNION, INTERSECT, ...
      if (query.SET) {
        // First arg may be leading query
        result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env, path.concat(['SET','args',0]))}`
        // FIXME: Clarify if set operators can be n-ary (assuming binary here)
        if (query.SET.op) {
          // Loop over all other arguments, i.e. for A UNION B UNION C UNION D ...
          for(let i = 1; i < query.SET.args.length; i++){
            result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[i], false, 'view', env, path.concat(['SET','args',i]))}`;
          }
        }
        result += ')';
        // Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to
        // each SELECT)
        if (query.SET.orderBy) {
          result += `${continueIndent(result, env)}order by ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
        }
        if (query.SET.limit) {
          result += `${continueIndent(result, env)}${renderLimit(query.SET.limit, env)}`;
        }
        return result;
      }
      // Otherwise must have a SELECT
      else if (!query.SELECT) {
        throw new Error('Unexpected query operation ' + JSON.stringify(query));
      }
      let select = query.SELECT;
      let childEnv = increaseIndent(env);
      if (options.forHana)
        childEnv.currentArtifactName = '$projection'; // $self to be replaced by $projection
      if (syntax === 'projection') {
        result += `projection on ${renderViewSource(select.from, env)}`;
      } else if (syntax === 'view' || syntax === 'entity') {
        result += `select from ${renderViewSource(select.from, env)}`;
      } else {
        throw new Error(`Unknown query syntax: ${syntax}`);
      }
      if (select.mixin) {
        let elems = '';
        for (let name in select.mixin) {
          if (!select.mixin[name]._ignore)
            elems += renderElement(name, select.mixin[name], childEnv);
        }
        if (elems) {
          result += ' mixin {\n';
          result += elems;
          result += env.indent + '} into';
        }
      }
      result += select.distinct ? ' distinct' : '';
      if (select.columns) {
        result += ' {\n';
        result += select.columns.map((col) => renderViewColumn(col, childEnv))
                                .filter(s => s !== '')
                                .join(',\n') + '\n';
        result += env.indent + '}';
      }
      if (select.excluding) {
        result += ' excluding {\n' + select.excluding.map(id => `${childEnv.indent}${quoteOrUppercaseId(id)}`).join(',\n') + '\n';
        result += env.indent + '}';
      }
      // FIXME: Currently, only projections can have actions and functions, but we cannot distinguish
      // a projection from a view any more
      if (isLeadingQuery) {
        result += renderActionsAndFunctions(query, env);
      }
      if (select.where) {
        result += `${continueIndent(result, env)}where ${renderExpr(select.where, env)}`;
      }
      if (select.groupBy) {
        result += `${continueIndent(result, env)}group by ${select.groupBy.map(expr => renderExpr(expr, env)).join(', ')}`;
      }
      if (select.having) {
        result += `${continueIndent(result, env)}having ${renderExpr(select.having, env)}`;
      }
      if (select.orderBy) {
        result += `${continueIndent(result, env)}order by ${select.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
      }
      if (select.limit) {
        result += `${continueIndent(result, env)}${renderLimit(select.limit, env)}`;
      }
      return result;
  
      // Utility function to make sure that we continue with the same indentation in WHERE, GROUP BY, ... after a closing curly brace and beyond
      function continueIndent(result, env) {
        if (result.endsWith('}') || result.endsWith('})')) {
          // The preceding clause ended with '}', just append after that
          return ' ';
        } else {
          // Otherwise, start new line and indent normally
          return '\n' + increaseIndent(env).indent;
        }
      }
  
      // Render a query's LIMIT clause, which may have also have OFFSET.
      function renderLimit(limit, env) {
        let result = '';
        if (limit.rows !== undefined) {
          result += `limit ${renderExpr(limit.rows, env)}`;
        }
        if (limit.offset !== undefined) {
          result += `${result !== '' ? '\n' + increaseIndent(env).indent : ''}offset ${renderExpr(limit.offset, env)}`;
        }
        return result;
      }
    }
  
    // Render one entry of a query's ORDER BY clause (which always has a 'value' expression, and may
    // have a 'sort' property for ASC/DESC and a 'nulls' for FIRST/LAST
    function renderOrderByEntry(entry, env) {
      let result = renderDocComment(entry, env) + renderExpr(entry, env);
      if (entry.sort) {
        result += ` ${entry.sort}`;
      }
      if (entry.nulls) {
        result += ` nulls ${entry.nulls}`;
      }
      return result;
    }
  
    // Render an entity's actions and functions (if any)
    // (expect an entity with trailing '}' or an 'extend' statement ending with 'with'
    // to have just been rendered).
    // Return the resulting source string.
    function renderActionsAndFunctions(art, env) {
      let result = '';
      let childEnv = increaseIndent(env);
      for (let name in art.actions) {
        result += renderDocComment(art.actions[name], childEnv) + renderActionOrFunction(name, art.actions[name], childEnv);
      }
      // Even if we have seen actions/functions, they might all have been ignored
      if (result !== '') {
        result = ' actions {\n' + result + env.indent + '}';
      }
      return result;
    }
  
    // Render an action or function 'act' with name 'actName'. Return the resulting source string.
    function renderActionOrFunction(actionName, act, env) {
      // Ignore if toHana says so
      if (act._ignore) {
        return '';
      }
      let result = renderDocComment(act, env) + renderAnnotationAssignments(act, env) + env.indent + act.kind + ' ' + env.namePrefix + quoteId(getLastPartOf(actionName));
      let childEnv = increaseIndent(env);
      let parameters = Object.keys(act.params || []).map(name => renderParameter(name, act.params[name], childEnv)).join(',\n');
      result += (parameters === '') ? '()' : '(\n' + parameters + '\n' + env.indent + ')';
      if (act.returns) {
        result += ' returns ' + renderTypeReference(act.returns, env) + renderNullability(act.returns);
      }
      result += ';\n';
      return result;
    }
  
    // Render an action or function parameter 'par' with name 'parName'. Return the resulting source string (no trailing LF).
    function renderParameter(parName, par, env) {
      let result = renderDocComment(par, env) + renderAnnotationAssignments(par, env) + env.indent + quoteOrUppercaseId(parName) + ' : ' + renderTypeReference(par, env);
      if(cdlMode && par.default) {
        result += ' default ' + renderExpr(par.default);
      }
      result += renderNullability(par);
      return result;
    }
  
    // Render a type (derived or structured) or an annotation decl with name 'artifactName'.
    // Return the resulting source string.
    function renderTypeOrAnnotation(artifactName, art, env) {
      let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
      result += env.indent + (art.dbType ? 'table ' : '') + ((art.$syntax && options.toCdl) ? art.$syntax : art.kind) + ' ' + env.namePrefix + quoteId(getLastPartOf(artifactName));
      if (art.includes) {
        // Includes are never flattened (don't exist in HANA)
        result += ' : ' + art.includes.map(name => renderAbsoluteNameWithQuotes(name, env)).join(', ');
      }
      let childEnv = increaseIndent(env);
      if (art.elements && !art.type) {
        // Structured type or annotation with anonymous struct type
        result += ' {\n';
        for (let name in art.elements) {
          result += renderElement(name, art.elements[name], childEnv);
        }
        result += env.indent + '};\n';
      } else {
        // Derived type or annotation with non-anonymous type
        result += ' : ' + renderTypeReference(art, env, false, true) + ';\n';
      }
      return result;
    }
  
    /**
     * Render a reference to a type used by 'elm' (named or inline)
     * Allow suppressing enum-rendering - used in columns for example
     *
     * @param {CSN.Element} elm
     * @param {object} env
     * @param {boolean} [noEnum=false]
     * @param {boolean} [renderWithColon=false]
     * @return {string}
     */
    function renderTypeReference(elm, env, noEnum=false, renderWithColon=cdlMode) {
      let result = renderDocComment(elm, env);
  
      // Array type: Render items instead
      if (elm.items && !elm.type) {
        // HANA CDS does not support keyword many
        let rc = (cdlMode ? 'many ' : 'array of ') + renderTypeReference(elm.items, env);
        if(elm.items.notNull != null)
          rc += elm.items.notNull ? ' not null' : ' null';
        return rc;
      }
  
      // FIXME: Is this a type attribute?
      result += (elm.localized ? 'localized ' : '');
  
      // Anonymous structured type
      if (!elm.type) {
        if (!elm.elements) {
          throw new Error('Missing type of: ' + JSON.stringify(elm));
        }
        result += '{\n';
        let childEnv = increaseIndent(env);
        for (let name in elm.elements) {
          result += renderElement(name, elm.elements[name], childEnv);
        }
        result += env.indent + '}'
        return result;
      }
  
      // Association type
      if (elm.type === 'cds.Association' || elm.type === 'cds.Composition') {
        // Type, cardinality and target
        result += elm.type === 'cds.Association' ? 'association' : 'composition';
        result += renderCardinality(elm.cardinality) + (elm.type === 'cds.Association' ? ' to ' : ' of ');
        // normal target or named aspect
        if(elm.target || elm.targetAspect && typeof elm.targetAspect === 'string') {
          result += plainNames ? renderAbsoluteNamePlain(elm.target || elm.targetAspect, env)
          : renderAbsoluteNameWithQuotes(elm.target || elm.targetAspect, env);
        } else if(elm.targetAspect && elm.targetAspect.elements) { // anonymous aspect
          const childEnv = increaseIndent(env);
          result += '{\n';
          for (let name in elm.targetAspect.elements) {
            result += renderElement(name, elm.targetAspect.elements[name], childEnv);
          }
          result += env.indent + '}';
        }
  
  
        // ON-condition (if any)
        if (elm.on) {
          result += ' on ' + renderExpr(elm.on, env);
        }
  
        // Foreign keys (if any, unless we also have an ON_condition (which means we have been transformed from managed to unmanaged)
        if (elm.keys && !elm.on) {
          result += ' { ' + Object.keys(elm.keys).map(name => renderForeignKey(elm.keys[name])).join(', ') + ' }';
        }
        return result;
      }
  
      // Reference to another element
      if (elm.type.ref && cdlMode) {
        if (elm.enum) {
          const source = artifactRef(elm.type);
          if(!source.enum){
              // enum was defined at this element and not at the referenced one
            result += renderAbsolutePath(elm.type, env, renderWithColon) + renderEnum(elm.enum, env);
          } else {
            result += renderAbsolutePath(elm.type, env, renderWithColon);
          }
        } else {
          result += renderAbsolutePath(elm.type, env, renderWithColon);
        }
        return result;
      } else if(elm.type.ref && options.forHana) {
        // For HANA CDS, we need a 'type of'
        result += 'type of ' + renderAbsolutePath(elm.type, env, false);
        if (elm.enum) {
          result += renderEnum(elm.enum, env);
        }
        return result;
      }
  
      // If we get here, it must be a named type
      if (isBuiltinType(elm.type)) {
        result += renderBuiltinType(elm);
      } else {
        // Simple absolute name
        // Type names are never flattened (derived types are unraveled in HANA)
        result += renderAbsoluteNameWithQuotes(elm.type, env);
      }
      if (elm.enum && !noEnum) {
        result += renderEnum(elm.enum, env);
      }
      return result;
    }
  
    function renderBuiltinType(elm) {
      // cds.Integer => render as Integer (no quotes)
      // Map Decimal (w/o Prec/Scale) to cds.DecimalFloat for HANA CDS
      if(options.toHana && elm.type === 'cds.Decimal' && elm.scale === undefined && elm.precision === undefined)
        return 'DecimalFloat';
      else {
        return elm.type.replace(/^cds\./, '') + renderTypeParameters(elm);
      }
    }
    // Render the 'enum { ... } part of a type declaration
    function renderEnum(enumPart, env) {
      let result = renderDocComment(enumPart, env) + ' enum {\n';
      const childEnv = increaseIndent(env);
      for (const name in enumPart) {
        const enumConst = enumPart[name];
        result += renderAnnotationAssignments(enumConst, childEnv);
        result += childEnv.indent + quoteId(name);
        if (enumConst.val !== undefined)
          result += ' = ' + renderExpr(enumConst, childEnv)
        result += ';\n';
      }
      result += env.indent + '}';
      return result;
    }
  
    // Render an annotation value (somewhat like a simplified expression, with slightly different
    // representation)
    function renderAnnotationValue(x, env) {
      if (x instanceof Array) {
        // Render array parts as values
        return `[${x.map(item => renderAnnotationValue(item, env)).join(', ')}]`;
      }
      else if (typeof x === 'object' && x !== null) {
        // Enum symbol
        if (x['#']) {
          return `#${x['#']}`;
        }
        // Shorthand for absolute path (as string)
        else if (x['=']) {
          return quotePathString(x['=']);
        }
        // Struct value (can actually only occur within an array)
        else {
          // Note that we have to quote the struct keys here manually and not use quoteId, because they may even contain dots (yuc!)
          // FIXME: Should that really be allowed?
          return `{${Object.keys(x).map(key => `![${key}]: ${renderAnnotationValue(x[key], env)}`).join(', ')}}`;
        }
      }
      // Null
      else if (x === null) {
        return 'null';
      }
      // Primitive: string, number, boolean
      else {
        // Quote strings, leave all others as they are
        return (typeof x === 'string') ? `'${x.replace(/'/g, "''")}'` : x;
      }
    }
  
    // Render an expression (including paths and values) or condition 'x'.
    // (no trailing LF, don't indent if inline)
    function renderExpr(x, env, inline=true, inExpr=false) {
      // Compound expression
      if (x instanceof Array) {
        // Simply concatenate array parts with spaces
        return x.map(item => renderExpr(item, env, inline, inExpr)).join(' ');
      }
      if (typeof x === 'object' && x !== null) {
        if (inExpr && x.cast && x.cast.type)
          return renderExplicitTypeCast(renderExprObject());
        return renderExprObject();
      }
      else {
        // Not a literal value but part of an operator, function etc - just leave as it is
        return x;
      }
  
      // Various special cases represented as objects
      function renderExprObject() {
        // Literal value, possibly with explicit 'literal' property
        if (x.val !== undefined) {
          switch (x.literal || typeof x.val) {
            case 'number':
            case 'boolean':
            case 'null':
              return x.val;
            case 'x':
            case 'date':
            case 'time':
            case 'timestamp':
              return `${x.literal}'${x.val}'`;
            case 'string':
              return `'${x.val.replace(/'/g, "''")}'`;
            case 'object':
              if (x.val === null) {
                return 'null';
              }
              // otherwise fall through to
            default:
              throw new Error('Unknown literal or type: ' + JSON.stringify(x));
          }
        }
        // Enum symbol
        else if (x['#']) {
          return `#${x['#']}`;
        }
        // Reference: Array of path steps, possibly preceded by ':'
        else if (x.ref) {
          if (options.forHana && !x.param && !x.global) {
            if(x.ref[0] === '$user') {
              // FIXME: this is all not enough: we might need an explicit select item alias
              if (x.ref[1] === 'id') {
                if (options.toHana.user && typeof options.toHana.user === 'string' || options.toHana.user instanceof String) {
                  return `'${options.toHana.user}'`;
                }
                else if ((options.toHana.user && options.toHana.user.id) && (typeof options.toHana.user.id === 'string' || options.toHana.user.id instanceof String)) {
                  return `'${options.toHana.user.id}'`;
                } else {
                  return "SESSION_CONTEXT('APPLICATIONUSER')";
                }
              }
              else if (x.ref[1] === 'locale')
                return "SESSION_CONTEXT('LOCALE')";
            }
            else if(x.ref[0] === '$at') {
              if(x.ref[1] === 'from') {
                return "SESSION_CONTEXT('VALID-FROM')";
              }
              else if(x.ref[1] === 'to') {
                return "SESSION_CONTEXT('VALID-TO')";
              }
            }
          }
          // FIXME: no extra magic with x.param or x.global
          return `${(x.param || x.global) ? ':' : ''}${x.ref.map(renderPathStep).join('.')}`;
        }
        // Function call, possibly with args (use '=>' for named args)
        else if (x.func) {
          // test for non-regular HANA identifier that needs to be quoted
          // identifier                  {letter}({letter_or_digit}|[#$])*
          // letter                      [A-Za-z_]
          // letter_or_digit             [A-Za-z_0-9]
  
          const regex = RegExp(/^[a-zA-Z][\w#$]*$/, 'g');
          const funcName = regex.test(x.func) ? x.func : quoteId(x.func);
          return renderFunc( funcName, x, (options.forHana ? 'hana' : 'cap'), a => renderArgs(a, '=>', env) );
        }
        // Nested expression
        else if (x.xpr) {
          return renderExpr(x.xpr, env, inline, true);
        }
        // Sub-select
        else if (x.SELECT) {
          // renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView)
          return `(${renderQuery(x, false, 'view', increaseIndent(env))})`;
        }
        else if (x.SET) {
          // renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource)
          return `${renderQuery(x, false, 'view', increaseIndent(env))}`;
        }
        else {
          throw new Error('Unknown expression: ' + JSON.stringify(x));
        }
      }
  
      /**
       * Renders an explicit `cast()` inside an 'xpr'.
       * @param {string} value
       */
      function renderExplicitTypeCast(value) {
        const typeRef = renderTypeReference(x.cast, env, true);
        if (options.forHana)
          return `CAST(${ value } AS ${ typeRef })`;
        else
          return `cast(${ value } as ${ typeRef })`;
      }
  
      // Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
      function renderPathStep(s, idx) {
        // Simple id or absolute name
        if (typeof(s) === 'string') {
          // HANA-specific extra magic (should actually be in forHana)
          if (options.forHana) {
            // In HANA, we replace leading $self by the absolute name of the current artifact
            // (see FIXME at renderArtifact)
            if (idx === 0 && s === '$self') {
              return plainNames ? renderAbsoluteNamePlain(env.currentArtifactName, env)
                                : renderAbsoluteNameWithQuotes(env.currentArtifactName, env);
            }
            // HANA-specific translation of '$now' and '$user'
            if (s === '$now') {
              return 'CURRENT_TIMESTAMP';
            }
          }
          // In first path position, do not quote $projection and magic $-variables like CURRENT_DATE, $now etc.
          // FIXME: We should rather explicitly recognize quoting somehow
  
          // TODO: quote $parameters if it doesn't reference a parameter, this requires knowledge about the kind
          // Example: both views are correct in HANA CDS
          // entity E { key id: Integer; }
          // view EV with parameters P1: Integer as select from E { id, $parameters.P1 };
          // view EVp as select from E as "$parameters" { "$parameters".id };
  
          if (idx === 0
              && s.startsWith('$')){
            return s;
          }
          return quoteOrUppercaseId(s);
        }
        // ID with filters or parameters
        else if (typeof s === 'object') {
          // Sanity check
          if (!s.func && !s.id) {
            throw new Error('Unknown path step object: ' + JSON.stringify(s));
          }
          // Not really a path step but an object-like function call
          if (s.func) {
            return `${s.func}(${renderArgs(s.args, '=>', env)})`;
          }
          // Path step, possibly with view parameters and/or filters
          let result = `${quoteOrUppercaseId(s.id)}`;
          if (s.args) {
            // View parameters
            result += `(${renderArgs(s.args, ':', env)})`;
          }
          if (s.where) {
            // Filter, possibly with cardinality
            result += `[${s.cardinality ? (s.cardinality.max + ': ') : ''}${renderExpr(s.where, env, inline, inExpr)}]`;
          }
          return result;
        }
        else {
          throw new Error('Unknown path step: ' + JSON.stringify(s));
        }
      }
    }
  
  
    // Render function arguments or view parameters (positional if array, named if object/dict),
    // using 'sep' as separator for positional parameters
    function renderArgs(args, sep, env) {
      // Positional arguments
      if (args instanceof Array) {
        return args.map(arg => renderExpr(arg, env)).join(', ');
      }
      // Named arguments (object/dict)
      else if (typeof args === 'object') {
        return Object.keys(args).map(key => `${quoteOrUppercaseId(key)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
      }
      else {
        throw new Error('Unknown args: ' + JSON.stringify(args));
      }
    }
  
    // Render a cardinality (only those parts that were actually provided)
    function renderCardinality(card) {
      if (!card) {
        return '';
      }
      let result = '[';
      if (card.src !== undefined) {
        result += card.src + ', ';
      }
      if (card.min !== undefined) {
        result += card.min + '..';
      }
      if (card.max !== undefined) {
        result += card.max;
      }
      return result + ']';
    }
  
    // Render the nullability of an element or parameter (can be unset, true, or false)
    function renderNullability(obj /* , env */) {
      if (obj.notNull === undefined) {
        // Attribute not set at all
        return '';
      }
      return obj.notNull ? ' not null' : ' null';
    }
  
    // Render a foreign key (no trailing LF)
    function renderForeignKey(fkey) {
      return `${renderExpr(fkey)}${fkey.as ? (' as ' + fkey.as) : ''}`;
    }
  
    // Render (primitive) type parameters of element 'elm', i.e.
    // length, precision and scale (even if incomplete), plus any other unknown ones.
    function renderTypeParameters(elm /*, env */) {
      let params = [];
      // Length, precision and scale (even if incomplete)
      if (elm.length !== undefined) {
        params.push(elm.length);
      }
      if (elm.precision !== undefined) {
        params.push(elm.precision);
      }
      if (elm.scale !== undefined) {
        params.push(elm.scale);
      }
      if (elm.srid !== undefined) {
        params.push(elm.srid);
      }
      // Additional type parameters
      // FIXME: Not yet clear how that looks in new CSN
      for (let arg of elm.typeArguments || []) {
        params.push(arg);
      }
      return params.length === 0 ? '' : '(' + params.join(', ') + ')';
    }
  
    // Render all annotation assignments of annotatable object 'obj'
    function renderAnnotationAssignments(obj, env) {
      let result = '';
      for (let name in obj) {
        if (name.startsWith('@')) {
          result += renderAnnotationAssignment(obj[name], name, env);
        }
      }
      return result;
    }
  
    // Render a single annotation assignment 'ann' with fully qualified name 'name' (no trailing LF).
    // We might see variants like 'A.B.C#foo' or even 'A.B#foo.C'. In both cases, the #foo must be ignored
    // when resolving the name, but must stay in the rendered output (quoted as necessary).
    function renderAnnotationAssignment(ann, name, env) {
      // If we are rendering for HANA CDS, never render any annotation (the only ones we haven't already
      // stripped when we come here are '@cds.persistence.*', which are meant to stay, but only in the CSN).
      if (options.toHana) {
        return '';
      }
      // Take the annotation assignment apart into <nameBeforeVariant>[#<variant>[.<nameAfterVariant>]]
      //                      @1111111  3333333   5555
      let parts = name.match(/@([^#]+)(#([^.]+)(\.(.+))?)?/);
      let nameBeforeVariant = parts[1];
      let variant = parts[3];
      let nameAfterVariant = parts[5];
      let topLevelName = getTopLevelArtifactNameOf(nameBeforeVariant, csn);
      let result = env.indent + '@';
      if (topLevelName) {
        // Checked annotation, with declaration - must render top-level absolute name with proper using and alias
        // Annotation names are never flattened
        result += renderAbsoluteNameWithQuotes(topLevelName, env);
        if (topLevelName != nameBeforeVariant) {
          result += '.' + quotePathString(nameBeforeVariant.substring(topLevelName.length + 1));
        }
      }
      else {
        // Unchecked annotation, just render the name as it is
        result += quotePathString(nameBeforeVariant);
      }
      // Append '#'-variant if any
      if (variant) {
        // FIXME: Unfortunately, the compiler does not yet understand an inner variant with proper quoting,
        // i.e. like "__A"."B"#"foo"."C". As a workaround, we present the '#'-variant as a quoted part of the
        // previous path step, i.e as "__A"."B#foo"."C" (which yields the same result). This hack is only necessary
        // for inner '#'-variants, i.e. for those followed by a <nameAfterVariant>.
        // FIXME: Won't work for inner variants on the top-level artifact, because the USING no longer matches
        // for something like "__A#foo"."B"."C"
        if (nameAfterVariant) {
          let resultSteps = result.split('.');
          // Take all paths steps from the result (which is now essentially 'nameBeforeVariant' with USING
          // adaptations) except the last step
          result = resultSteps.slice(0, -1).join('.');
          // Append a combination of last path step and '#'-variant (quoted)
          let lastStep = resultSteps[resultSteps.length - 1];
          if (lastStep.includes('"')) {
            // Last step was already quoted - strip off the existing quotes
            lastStep = lastStep.slice(1, -1);
          }
          result += '.' + quoteId(lastStep + '#' + variant);
        }
        else {
          // No hack required for trailing '#'-variant
          result += '#' + quoteId(variant);
        }
      }
      // Append anything that might have come after the variant
      if (nameAfterVariant) {
        result += '.' + quotePathString(nameAfterVariant);
      }
      result += ' : ' + renderAnnotationValue(ann, env);
      return result  + '\n';
    }
  
    // Render an absolute name in 'plain' mode, i.e. uppercased and underscored. Also record the
    // fact that 'absName' is used in 'env', so that an appropriate USING can be constructed
    // if necessary.
    function renderAbsoluteNamePlain(absName, env) {
      // Add using declaration
      env.topLevelAliases[absName] = {
        quotedName: uppercaseAndUnderscore(absName),
        quotedAlias: uppercaseAndUnderscore(absName)
      };
      return uppercaseAndUnderscore(absName);
    }
  
    // Render an absolute name 'absName', with appropriate quotes. Also record the
    // fact that 'absName' is used in 'env', so that an appropriate USING can be constructed
    // if necessary.
    function renderAbsoluteNameWithQuotes(absName, env) {
      // Special case: If the top-level artifact name is not a valid artifact name, it came from an unchecked annotation
      // and must be left as it is (just quoted)
      let topLevelName = getTopLevelArtifactNameOf(absName, csn);
      if (!csn.definitions[topLevelName]) {
        return quotePathString(absName);
      }
  
      // Another special case: If we are rendering for HANA, and if the first path step is an artifact that is
      // 'implemented in' something, we need to treat the whole name like a top-level id.
      if (options.toHana && csn.definitions[absName] && csn.definitions[absName]['@cds.persistence.exists']) {
        env.topLevelAliases[absName] = {
          quotedName: quoteAbsoluteNameAsId(absName),
          quotedAlias: quoteId(createTopLevelAliasName(absName))
        };
        return env.topLevelAliases[absName].quotedAlias;
      }
  
      // Retrieve or create a suitable alias name for the surrounding top-level artifact
      let topLevelAlias = env.topLevelAliases[topLevelName];
      if (!topLevelAlias) {
        env.topLevelAliases[topLevelName] = {
          quotedName: quoteAbsolutePathString(topLevelName),
          quotedAlias: quoteId(createTopLevelAliasName(topLevelName))
        };
        topLevelAlias = env.topLevelAliases[topLevelName];
      }
  
      // Replace the top-level name with its alias
      if (absName == topLevelName) {
        return topLevelAlias.quotedAlias;
      } else {
        return topLevelAlias.quotedAlias + '.' + quotePathString(absName.substring(topLevelName.length + 1));
      }
    }
  
    // Create a suitable alias name for a top-level artifact name. Ideally, it should not conflict with
    // any other identifier in the model and be somewhat recognizable and un-ugly...
    function createTopLevelAliasName(topLevelName) {
      // FIXME: We should rather check for conflicts than just using something obscure like this ...
      return '__' + topLevelName.replace(/::/g, '__').replace(/\./g, '_');
    }
  
    // Render appropriate USING directives for all artifacts used by artifact 'artifactName' in 'env'.
    function renderUsings(artifactName, env) {
      return Object.keys(env.topLevelAliases)
                   .filter(name => !(plainNames && env.topLevelAliases[name].quotedName == uppercaseAndUnderscore(artifactName))) // avoid "using FOO as FOO" in FOO.cds
                   .map(name => 'using ' + env.topLevelAliases[name].quotedName + ' as ' + env.topLevelAliases[name].quotedAlias + ';\n').join('');
    }
  
    // Depending on the naming style, render the namespace declaration for a top-level artifact 'name'
    // if it has a namespace parent. Assume that this is only called for top-level artifacts.
    // - For 'quoted' and 'hdbcds' names, render the namespace declaration (resulting in '.' or '::' style names)
    // - For 'plain' names, do not render anything (namespace already part of flattened names).
    // Return the namespace declaration (with trailing LF) or an empty string.
    function renderNamespaceDeclaration(topLevelName, env) {
      if (plainNames) {
        // No namespaces in plain mode
        return '';
      }
      // The top-level artifact's parent would be the namespace (if any)
      let namespace = getParentNameOf(topLevelName);
      if (namespace) {
        return env.indent + 'namespace ' + quotePathString(namespace) + ';\n';
      }
      return '';
    }
  
    // Return a dictionary of top-level artifacts contained in the model (by their name)
    function getTopLevelArtifacts() {
      let result = Object.create(null);
      for (let name in csn.definitions) {
        if (plainNames) {
          let art = csn.definitions[name];
          // For 'plain' naming, take all entities and views, nothing else
          if (art.kind === 'entity' || art.kind === 'view') {
            result[name] = art;
          }
        }
        else {
          // For all other naming conventions, take all top-level artifacts except namespaces
          let topLevelName = getTopLevelArtifactNameOf(name, csn);
          let topLevelArtifact = csn.definitions[topLevelName];
          if (topLevelArtifact && topLevelArtifact.kind !== 'namespace') {
            result[topLevelName] = topLevelArtifact;
          }
        }
      }
      return result;
    }
  
    // Returns a newly created default environment (which keeps track of indentation, required USING
    // declarations and name prefixes.
    function createEnv() {
      return {
        // Current indentation string
        indent: '',
        // Dictionary of aliases for used artifact names, each entry like 'name' : { quotedName, quotedAlias }
        topLevelAliases: Object.create(null),
        // Current name prefix (including trailing dot if not empty)
        namePrefix: ''
      }
    }
  
    // Returns a copy of 'env' with (quoted) name prefix 'id' and a dot appended to the current name prefix
    function addNamePrefix(env, id) {
      return Object.assign({}, env, { namePrefix: env.namePrefix + quoteId(id) + '.' });
    }
  
    // Returns a copy of 'env' with increased indentation (and resetted name prefix)
    function increaseIndent(env) {
      return Object.assign({}, env, { namePrefix: '', indent: env.indent + '  ' });
    }
  
    // Return a path string 'path' with appropriate "-quotes.
    function quotePathString(path) {
      // "foo"."bar"."wiz"."blub"
      return path.split('.').map(quoteId).join('.');
    }
  
    // Return an absolute path 'abspath', with '::' inserted if required by naming strategy 'hdbcds',
    // with appropriate "-quotes
    function quoteAbsolutePathString(abspath) {
      if (!hdbcdsNames) {
        return quotePathString(abspath);
      }
      let topLevelName = getTopLevelArtifactNameOf(abspath, csn);
      let namespace = getParentNameOf(topLevelName);
      if (namespace) {
        return quotePathString(namespace) + '::' + quotePathString(abspath.substring(namespace.length + 1));
      }
      return quotePathString(abspath);
    }
  
    // Return an id 'id' with appropriate "-quotes
    function quoteId(id) {
      // Should only ever be called for real IDs (i.e. no dots inside)
      if (id.indexOf('.') != -1) {
        throw new Error(id);
      }
      // FIXME: Somewhat arbitrary magic: Do not quote $projection (because HANA CDS doesn't recognize it otherwise).  Similar for $self.
      // FIXME: The test should not be on the name, but by checking the _artifact.
      if (id === '$projection' || id === '$self' ||
          id === '$user' || id === '$now') {
        return id;
      }
  
      // Quote if required for CDL or (if rendering for toHana) always
      if (requiresQuotingForCdl(id) || options.toHana) {
        // Sanity check
        if (plainNames) {
          //throw new Error('Not expecting quotes in plain mode');
        }
        if(cdlMode)
          return `![${id.replace(/]/g, ']]')}]`;
        else
          return `"${id.replace(/"/g, '""')}"`;
      }
      return id;
    }
  
    // Returns true if 'id' requires quotes for CDL, i.e. if 'id'
    // 1. starts with a digit
    // 2. contains chars different than:
    //  - uppercase letters
    //  - lowercase letters
    //  - digits
    //  - underscore
    // 3. is a CDL keyword or a CDL function without parentheses (CURRENT_*, SYSUUID, ...)
    function requiresQuotingForCdl(id) {
      return id.match(/^\d/)
          || id.match(/\W/g)
          || keywords.cdl.includes(id.toUpperCase())
          || keywords.cdl_functions.includes(id.toUpperCase());
    }
  
    // Return an absolute name 'absname', with '::' inserted if required by naming strategy 'hdbcds', quoted
    // as if it was a single identifier (required only for native USINGs)
    function quoteAbsoluteNameAsId(absname) {
      if (hdbcdsNames) {
        let topLevelName = getTopLevelArtifactNameOf(absname, csn);
        let namespace = getParentNameOf(topLevelName);
        if (namespace) {
          return '"' + (namespace + '::' + absname.substring(namespace.length + 1)).replace(/"/g, '""') + '"';
        }
      }
      if(cdlMode)
        return '![' + absname.replace(/]/g, ']]') + ']';
      else
        return '"' + absname.replace(/"/g, '""') + '"';
    }
  
    /**
     * Quote or uppercase an identifier 'id', depending on naming strategy
     *
     * @param {string} id
     * @param {CSN.Location} [location] Optional location for the warning.
     * @return {string}
     */
    function quoteOrUppercaseId(id, location=null) {
      if (plainNames) {
        // Sanity check
        if (!options.toHana) {
          throw new Error('Not expecting uppercase names in non-HANA mode');
        }
        let result = uppercaseAndUnderscore(id);
        // Warn if colliding with HANA keyword
        if (keywords.hana.includes(result)) {
          signal(warning`The identifier "${id}" is a HANA keyword.`, location);
        }
        return result;
      } else {
        return quoteId(id);
      }
    }
  
    // Render the name of an artifact, using the current name prefix from 'env'
    // and just the last part of the artifact's name. In case of plain names, this
    // is equivalent to simply flattening and uppercasing the whole name.
    function renderArtifactName(artifactName, env) {
      return (plainNames) ? uppercaseAndUnderscore(artifactName)
                          : env.namePrefix + quoteId(getLastPartOf(artifactName));
    }
  
    // For 'name', replace '.' by '_', convert to uppercase, and add double-quotes if
    // required because of non-leading '$' (but do not consider leading '$', other special
    // characters, or SQL keywords/functions - somewhat weird but this retains maximum
    // compatibility with a future hdbtable-based solution and with sqlite, where non-leading
    // '$' is legal again but nothing else)
    function uppercaseAndUnderscore(name) {
      // Always replace '.' by '_' and uppercase
      return name.replace(/\./g, '_').toUpperCase();
    }
  }
  
  module.exports = { toCdsSource, toCdsSourceCsn };
  