text: const parseXml = require('./xmlParserWithLocations');
  
  // Predefined augmented csn parts
  const augmented = {
    extensions: () => {
      return {
        kind: 'source',
        extensions: [],
        messages: [],
        $frontend: 'xml',
      }
    },
    extension: (key, location) => {
      return {
        location,
        name: { path: [{ id: key, location }], location },
        annotationAssignments: [],
        kind: 'annotate'
      }
    }
  }
  
  /*
  
    The generator takes an xml file as an input and generates augmented csn extenstions as per the content.
    The logic assumes that the xml content is a valid edmx one, e.g. has 'Annotations' tags with 'Target' attributes
    and those 'Annotations' tags have 'Annotation' tags for children.
    Take in mind, that the logic "reserves" the name 'EntityContainer' and if in the base model there is such an entity
    the generated extensions might be inapplicable.
  */
  function edmx2csn(src, filename, options) {
    let source = parseXml(src, filename);
  
    // create the skeleton for the augmented result
    let csn = augmented.extensions();
  
    // call 'processAnnotationsTag' method for each "Annotations" tag from the
    // input xml tree(the declarations in interest lie there)
    forEachAnnotationsTag(source);
    // The EDMX parser do not use the normal message function:
    if (options.messages && csn.messages)
      options.messages.push(...csn.messages);
    return csn;
  
    // goes recursively through the parsed xml object and call process the 'Annotations' tags
    function forEachAnnotationsTag(obj) {
      if (obj === null)
        return;
      for (let prop in obj) {
        if (prop === 'Schema') {
          processSchemaTag(obj['Schema'][0], csn);
        }
        obj[prop].forEach(elem => forEachAnnotationsTag(elem));
      }
    }
  }
  
  // An edmx document has a 'Schema' tag and inside of it are the 'Annotation(s)' tags.
  // Each 'Annotations' tag has an attribute 'Target' denoting the artifact from the model to be annotated.
  // This method creates the extension for this artifact with the content from the child 'Annotation' tag(s)
  // for each 'Annotations' tag from the xml input.
  //
  // If there is a direct child of the 'Schema' tag,
  // which is 'Annotation' tag, then this annotation belongs to the service.
  function processSchemaTag(schema, csn) {
    // process top level/schema level 'Annotation' tag
    if (schema.Annotation) {
      let serviceName = schema._attributes.Namespace;
      schema.Annotation.forEach(obj => {
        let extension = augmented.extension(serviceName, obj._location);
        Object.assign(extension, augmentedExtensionFromEdmxAnno(obj));
        csn.extensions.push(extension);
      });
    }
  
    // loop through all the 'Annotations' tags in the source
    schema['Annotations'].forEach(obj => {
      if (obj._selfClosing)
        return;
  
      // the value has the form of 'ServiceName['.EntityContainer/' or '.']EntityName[/elem1/elem2]'
      let elements, topLevelArt;
      if (obj._attributes.Target.includes('.EntityContainer')) {
        // handle here cases like:
        // MyService.EntityContainer -> annotate MyService
        // MyService.EntityContainer/MyEntity -> annotate MyService.MyEntity
        let [root, elements] = obj._attributes.Target.split('/');
        // remove trailing 'EntityContainer'
        topLevelArt = root.split('.').slice(0, -1).join('.');
        if (elements) {
          if (!Array.isArray(elements)) elements = elements.split();
          topLevelArt = topLevelArt.concat(`.${elements[0]}`)
          if (elements.length > 1) elements.shift();
        }
      }
      else {
        elements = obj._attributes.Target.split('/');
        topLevelArt = elements.shift(); // get the top level targeted artifact
      }
      // 1. Determine which artifact to be annotated by the 'Target' attribute
      // look if there is an extension for this top level artifact already
      let toBeAnnotated = csn.extensions.find(ext => ext.name.absolute === topLevelArt);
      if (!toBeAnnotated) { // if not -> create one
        toBeAnnotated = augmented.extension(topLevelArt, obj._location);
        csn.extensions.push(toBeAnnotated);
      }
      // The extension to be annotated can be also a child element of a top level artifact
      if (elements) elements.forEach(elem => {
        toBeAnnotated.elements = toBeAnnotated.elements || Object.create(null);
        toBeAnnotated = toBeAnnotated.elements[elem]
          ? toBeAnnotated.elements[elem]
          : toBeAnnotated.elements[elem] = augmented.extension(elem, obj._location);
      })
  
      // 2. Generate and assign the augmented annotations for this specific 'Target'
      // loop through all the 'Annotation' tags under an 'Annotations' tag
      // and assign the augmented form of the annotation
      obj.Annotation.forEach(annotion => {
        Object.assign(toBeAnnotated, augmentedExtensionFromEdmxAnno(annotion));
      });
    });
  }
  
  // takes an 'Annotation' tag object of a parsed xml and creates annotation assignment in the extension from it
  // last two arguments(messages, targetName) are used for logging during valdiation of an edmx annotation definition
  function augmentedExtensionFromEdmxAnno(anno) {
  
    let parsedAnnos = handleGeneric({ 'Annotation': [anno] });
  
    let result = Object.create(null);
    let annoName = `@${anno._attributes.Term}${anno._attributes.Qualifier ? `#${anno._attributes.Qualifier}` : ''}`;
  
    if (parsedAnnos.struct) { //from struct create all the annotations
      // TODO: nested elementes(Record in a Record) check if such a case exists
      Object.keys(parsedAnnos.struct).forEach(a => {
        result[`${annoName}.${a}`] = parsedAnnos.struct[a];
        setNameAndPriority(result[`${annoName}.${a}`], `@${anno._attributes.Term}.${a}`);
      });
    } else {
      result[annoName] = parsedAnnos;
      setNameAndPriority(result[annoName], anno._attributes.Term);
    }
  
    function setNameAndPriority(obj, name) {
      obj.name = {
        path: name.split('.').map(p => {
          return { id: p, location: anno._location }
        }), location: anno._location
      }
      obj.priority = 'annotate';
    }
  
    return result;
  
    // actual parsing of an edmx annotation definition
    function handleGeneric(obj, isCollection = false) {
      // Predefined edmx attributes to csn conversion relations
      const edmxAttrs = {
        Bool: val => {
          return {
            literal: 'boolean',
            val: val === 'true'
          }
        },
        String: val => {
          return {
            literal: 'string',
            val: val
          }
        },
        Decimal: val => {
          return {
            literal: 'number',
            val: Number.isSafeInteger(Number.parseFloat(val)) ? Number(val) : val
          }
        },
        Float: val => {
          return {
            literal: 'number',
            val: Number.isSafeInteger(Number.parseFloat(val)) ? Number(val) : val
          }
        },
        Int: val => {
          return {
            literal: 'number',
            val: Number.isSafeInteger(Number.parseInt(val)) ? Number(val) : val
          }
        },
        Binary: val => edmxAttrs.String(val),
        Date: val => edmxAttrs.String(val),
        DateTimeOffset: val => edmxAttrs.String(val),
        Guid: val => edmxAttrs.String(val),
        TimeOfDay: val => edmxAttrs.String(val),
        Duration: val => edmxAttrs.String(val),
        EnumMember: (obj, location) => handleEnum(obj, location),
        Path: (obj, location) => handlePath(obj, false, location),
        NavigationPropertyPath: obj => handlePath(obj, false),
        PropertyPath: obj => handlePath(obj, false)
      }
  
      if (obj.Annotation)
        return handleAnnotation(obj.Annotation, isCollection);
      if (obj.Record)
        return handleRecord(obj.Record, isCollection);
      if (obj.Collection)
        return handleCollection(obj.Collection);
      if (getPath(obj))
        return handlePath(getPath(obj), isCollection);
      if (obj.PropertyValue)
        return handlePropValue(obj.PropertyValue, isCollection);
      else
        return handleValue(obj, isCollection);
  
      function getPath(obj) {
        return obj.PropertyPath || obj.NavigationPropertyPath || obj.AnnotationPath;
      }
  
      function handleAnnotation(obj, isCollection) {
        if (isCollection)
          return obj.map(e => handleAnnotation(e, false));
        obj = obj[0] || obj;
        // handle a nested annotation in an annotation
        if (obj.Annotation) {
          // assign the value
          let result = { literal: 'struct', location: obj._location, struct: {} };
          result.struct['$value'] = getPath(obj) ? handlePath(getPath(obj)) : handleValue(obj, false);
          result.struct['$value'].name = { id: '$value', location: obj._location };
          // assign the nested annotations
          obj.Annotation.forEach(anno => {
            let term = anno._attributes.Term;
            result.struct[`@${term}`] = { name: { id: term, location: anno._location } };
            Object.assign(result.struct[`@${term}`], handleGeneric(anno, false));
          });
          return result;
        } else
          return handleGeneric(obj, isCollection);
      }
  
      function handleRecord(obj, isCollection) {
        if (isCollection)
          return obj.map(e => handleRecord(e, false))
        let record = obj[0] || obj;
        let result = {
          literal: 'struct',
          location: record._location,
          struct: Object.create(null)
        }
        // a record can have as children either PropertyValue or Annotation
        if (record.PropertyValue)
          record.PropertyValue.forEach(prop => {
            Object.assign(result.struct, handlePropValue(prop, false));
          })
        if (record.Annotation)
          record.Annotation.forEach(anno => {
            result.struct[`@${anno._attributes.Term}`] = {
              name: {
                id: `@${anno._attributes.Term}`,
                location: anno._location
              }
            };
            Object.assign(result.struct[`@${anno._attributes.Term}`], handleGeneric(anno, false));
          })
        return result;
      }
  
      function handleCollection(collection) {
        // edmx annotation cannot have Collection of Collection (checked in Dictionary.json),
        // so will have always one Collection tag as a child tag
        return {
          literal: 'array',
          location: collection[0]._location,
          val: handleGeneric(collection[0], true)
        };
      }
  
      function handlePath(path, isCollection, location = obj._location) {
        if (isCollection)
          return path.map(e => handlePath(e, false, location))
        return {
          path: (path._text || path).split('/').map(p => { return { id: p, location: location } }),
          location: obj._location
        };
      }
  
      function handlePropValue(obj, isCollection) {
        if (isCollection)
          return obj.map(e => handlePropValue(e, false))
        let result = { [obj._attributes.Property]: handleGeneric(obj) };
        result[obj._attributes.Property].name = { id: obj._attributes.Property, location: obj._location };
        return result;
      }
  
      function handleValue(obj, isCollection) {
        // this prevents the map execution of not an array, valid only for the collection tag
        // the validation throws a specific error for the empty collection
        if (isCollection && obj._selfClosing)
          return [];
  
        // handle the case of builtin as tag and collection of builtins
        let builtinTag = Object.keys(edmxAttrs).find(atr => Object.keys(obj).includes(atr) || atr === obj._name);
        if (builtinTag) {
          if (isCollection)
            return obj[builtinTag].map(e => handleValue(e, false));
          return Object.assign(edmxAttrs[builtinTag](obj._text), { location: obj._location });
        }
  
        if (isCollection)
          return obj.map(e => handleValue(e, false));
  
        // if (obj._attributes.EnumMember)
        //   return handleEnum(obj._attributes.EnumMember, obj._location)
  
        let bltnAttr = Object.keys(edmxAttrs)
          .filter(attr => -1 !== Object.keys(obj._attributes).indexOf(attr))[0];
        if (bltnAttr)
          return Object.assign(edmxAttrs[bltnAttr](obj._attributes[bltnAttr], obj._location), { location: obj._location });
        return {};
      }
  
      function handleEnum(enummember, location) {
        // the value contains whitespaces => array
        if (/\s/.test(enummember))
          return { literal: 'array', location, val: enummember.match(/\S+/g).map(e => handleEnum(e, location)) };
        let enumSymbol = enummember.split('/').pop();
        return {
          literal: 'enum',
          symbol: { id: enumSymbol, location },
          location
        }
      }
    }
  }
  
  module.exports = edmx2csn;
  