text: "use strict";
  
  var ok = require("assert").ok;
  var taglibTypes = require("../taglib-loader/types");
  var extend = require("raptor-util/extend");
  var hasOwnProperty = Object.prototype.hasOwnProperty;
  
  function transformerComparator(a, b) {
    a = a.priority;
    b = b.priority;
  
    if (a == null) {
      a = Number.MAX_VALUE;
    }
  
    if (b == null) {
      b = Number.MAX_VALUE;
    }
  
    return a - b;
  }
  
  function TAG_COMPARATOR(a, b) {
    return a.name.localeCompare(b.name);
  }
  
  function merge(target, source) {
    for (var k in source) {
      if (hasOwnProperty.call(source, k)) {
        if (
          target[k] &&
          typeof target[k] === "object" &&
          source[k] &&
          typeof source[k] === "object"
        ) {
          if (source.__noMerge) {
            // Don't merge objects that are explicitly marked as "do not merge"
            continue;
          }
  
          if (Array.isArray(target[k]) || Array.isArray(source[k])) {
            var targetArray = target[k];
            var sourceArray = source[k];
  
            if (!Array.isArray(targetArray)) {
              targetArray = [targetArray];
            }
  
            if (!Array.isArray(sourceArray)) {
              sourceArray = [sourceArray];
            }
  
            target[k] = [].concat(targetArray).concat(sourceArray);
          } else {
            var Ctor = target[k].constructor;
            var newTarget = new Ctor();
            merge(newTarget, target[k]);
            merge(newTarget, source[k]);
            target[k] = newTarget;
          }
        } else {
          target[k] = source[k];
        }
      }
    }
  
    return target;
  }
  
  /**
   * A taglib lookup merges in multiple taglibs so there is a single and fast lookup
   * for custom tags and custom attributes.
   */
  class TaglibLookup {
    constructor() {
      this.merged = {
        attributeGroups: {}
      };
      this.taglibsById = {};
  
      this._sortedTags = undefined;
    }
  
    hasTaglib(taglib) {
      return hasOwnProperty.call(this.taglibsById, taglib.id);
    }
  
    _mergeNestedTags(taglib) {
      var Tag = taglibTypes.Tag;
      // Loop over all of the nested tags and register a new custom tag
      // with the fully qualified name
  
      var merged = this.merged;
  
      function handleNestedTags(tag, parentTagName) {
        tag.forEachNestedTag(function(nestedTag) {
          var fullyQualifiedName = parentTagName + ":" + nestedTag.name;
          // Create a clone of the nested tag since we need to add some new
          // properties
          var clonedNestedTag = new Tag();
          extend(clonedNestedTag, nestedTag);
          // Record the fully qualified name of the parent tag that this
          // custom tag is associated with.
          clonedNestedTag.parentTagName = parentTagName;
          clonedNestedTag.name = fullyQualifiedName;
          merged.tags[fullyQualifiedName] = clonedNestedTag;
          handleNestedTags(clonedNestedTag, fullyQualifiedName);
        });
      }
  
      taglib.forEachTag(function(tag) {
        handleNestedTags(tag, tag.name);
      });
    }
  
    addTaglib(taglib) {
      ok(taglib, '"taglib" is required');
      ok(taglib.id, '"taglib.id" expected');
  
      if (hasOwnProperty.call(this.taglibsById, taglib.id)) {
        return;
      }
  
      // console.log("TAGLIB:", taglib);
  
      this._sortedTags = undefined;
  
      this.taglibsById[taglib.id] = taglib;
  
      merge(this.merged, {
        tags: taglib.tags,
        transformers: taglib.transformers,
        attributes: taglib.attributes,
        patternAttributes: taglib.patternAttributes,
        attributeGroups: taglib.attributeGroups || {}
      });
  
      this._mergeNestedTags(taglib);
    }
  
    getTagsSorted() {
      var sortedTags = this._sortedTags;
  
      if (sortedTags === undefined) {
        sortedTags = this._sortedTags = [];
        this.forEachTag(tag => {
          sortedTags.push(tag);
        });
        sortedTags.sort(TAG_COMPARATOR);
      }
  
      return sortedTags;
    }
  
    forEachTag(callback) {
      var tags = this.merged.tags;
      if (tags) {
        for (var tagName in tags) {
          if (hasOwnProperty.call(tags, tagName)) {
            var tag = tags[tagName];
            var result = callback(tag);
            if (result === false) {
              break;
            }
          }
        }
      }
    }
  
    forEachAttribute(tagName, callback) {
      var tags = this.merged.tags;
      if (!tags) {
        return;
      }
  
      var globalAttributes = this.merged.attributes;
      var taglibAttributeGroups = this.merged.attributeGroups;
  
      function findAttributesForTagName(tagName) {
        var tag = tags[tagName];
        if (!tag) {
          return;
        }
  
        function handleAttr(attrDef) {
          if (attrDef.ref) {
            attrDef = globalAttributes[attrDef.ref];
          }
          callback(attrDef, tag);
        }
  
        var attributes = tag.attributes;
        if (!attributes) {
          return;
        }
  
        for (var attrName in attributes) {
          if (hasOwnProperty.call(attributes, attrName)) {
            handleAttr(attributes[attrName], tag);
          }
        }
  
        if (tag.attributeGroups) {
          for (let i = 0; i < tag.attributeGroups.length; i++) {
            let attributeGroupName = tag.attributeGroups[i];
            let attributeGroup = taglibAttributeGroups[attributeGroupName];
            if (attributeGroup) {
              for (let attrName in attributeGroup) {
                handleAttr(attributeGroup[attrName]);
              }
            }
          }
        }
  
        if (tag.patternAttributes) {
          tag.patternAttributes.forEach(handleAttr);
        }
      }
  
      findAttributesForTagName(tagName); // Look for an exact match at the tag level
      findAttributesForTagName("*"); // Including attributes that apply to all tags
    }
  
    getTag(element) {
      var tags = this.merged.tags;
      if (!tags) {
        return;
      }
  
      return tags[element.tagName || element];
    }
  
    getAttribute(element, attr) {
      if (typeof element === "string") {
        element = {
          tagName: element
        };
      }
  
      if (typeof attr === "string") {
        attr = {
          name: attr
        };
      }
  
      var tags = this.merged.tags;
      if (!tags) {
        return;
      }
  
      var taglibAttributeGroups = this.merged.attributeGroups;
  
      var tagName = element.tagName;
      var attrName = attr.name;
  
      function findAttributeForTag(tag, attributes, attrName) {
        // try by exact match first
        var attribute = attributes[attrName];
        if (attribute === undefined) {
          if (tag.attributeGroups) {
            for (let i = 0; i < tag.attributeGroups.length; i++) {
              let attributeGroupName = tag.attributeGroups[i];
              let attributeGroup = taglibAttributeGroups[attributeGroupName];
              if (attributeGroup) {
                attribute = attributeGroup[attrName];
                if (attribute !== undefined) {
                  break;
                }
              }
            }
          }
        }
  
        if (attribute === undefined && attrName !== "*") {
          if (tag.patternAttributes) {
            // try searching by pattern
            for (var i = 0, len = tag.patternAttributes.length; i < len; i++) {
              var patternAttribute = tag.patternAttributes[i];
              if (patternAttribute.pattern.test(attrName)) {
                attribute = patternAttribute;
                break;
              }
            }
          }
        }
  
        return attribute;
      }
  
      var globalAttributes = this.merged.attributes;
  
      function tryAttribute(tagName, attrName) {
        var tag = tags[tagName];
        if (!tag) {
          return undefined;
        }
  
        return findAttributeForTag(tag, tag.attributes, attrName);
      }
  
      var attrDef =
        tryAttribute(tagName, attrName) || // Look for an exact match at the tag level
        tryAttribute("*", attrName) || // If not there, see if there is a exact match on the attribute name for attributes that apply to all tags
        tryAttribute(tagName, "*"); // Otherwise, see if there is a splat attribute for the tag
  
      if (attrDef && attrDef.ref) {
        attrDef = globalAttributes[attrDef.ref];
      }
  
      return attrDef;
    }
  
    forEachTemplateMigrator(callback, thisObj) {
      for (var key in this.taglibsById) {
        var migration = this.taglibsById[key].migratorPath;
        if (migration) {
          callback.call(thisObj, migration);
        }
      }
    }
  
    forEachTagMigrator(element, callback, thisObj) {
      if (typeof element === "string") {
        element = {
          tagName: element
        };
      }
  
      var tagName = element.tagName;
      /*
       * If the node is an element node then we need to find all matching
       * migrators based on the URI and the local name of the element.
       */
  
      var migrators = [];
  
      function addMigrator(migrator) {
        if (typeof migrator !== "function") {
          throw new Error("Invalid transformer");
        }
  
        migrators.push(migrator);
      }
  
      /*
       * Handle all of the migrators for all possible matching migrators.
       *
       * Start with the least specific and end with the most specific.
       */
  
      if (this.merged.tags) {
        if (tagName) {
          if (this.merged.tags[tagName]) {
            this.merged.tags[tagName].forEachMigrator(addMigrator);
          }
        }
  
        if (this.merged.tags["*"]) {
          this.merged.tags["*"].forEachMigrator(addMigrator);
        }
      }
  
      migrators.forEach(callback, thisObj);
    }
  
    forEachTemplateTransformer(callback, thisObj) {
      var transformers = this.merged.transformers;
      if (transformers && transformers.length) {
        transformers.forEach(callback, thisObj);
      }
    }
  
    forEachTagTransformer(element, callback, thisObj) {
      if (typeof element === "string") {
        element = {
          tagName: element
        };
      }
  
      var tagName = element.tagName;
      /*
       * If the node is an element node then we need to find all matching
       * transformers based on the URI and the local name of the element.
       */
  
      var transformers = [];
  
      function addTransformer(transformer) {
        if (!transformer || !transformer.path) {
          throw new Error("Invalid transformer");
        }
  
        transformers.push(transformer);
      }
  
      /*
       * Handle all of the transformers for all possible matching transformers.
       *
       * Start with the least specific and end with the most specific.
       */
  
      if (this.merged.tags) {
        if (tagName) {
          if (this.merged.tags[tagName]) {
            this.merged.tags[tagName].forEachTransformer(addTransformer);
          }
        }
  
        if (this.merged.tags["*"]) {
          this.merged.tags["*"].forEachTransformer(addTransformer);
        }
      }
  
      transformers.sort(transformerComparator);
  
      transformers.forEach(callback, thisObj);
    }
  
    forEachTextTransformer(callback, thisObj) {
      if (this.merged.textTransformers) {
        this.merged.textTransformers.sort(transformerComparator);
        this.merged.textTransformers.forEach(callback, thisObj);
      }
    }
  
    toString() {
      return "lookup: " + Object.keys(this.taglibsById).join(", ");
    }
  }
  
  module.exports = TaglibLookup;
  