text: "use strict";
  
  var fs = require("fs");
  
  // output stream
  var out = null;
  
  // documentation data
  var data = null;
  
  // already handled objects, by name
  var seen = {};
  
  // indentation level
  var indent = 0;
  
  // whether indent has been written for the current line yet
  var indentWritten = false;
  
  // provided options
  var options = {};
  
  // queued interfaces
  var queuedInterfaces = [];
  
  // whether writing the first line
  var firstLine = true;
  
  // JSDoc hook
  exports.publish = function publish(taffy, opts) {
      options = opts || {};
  
      // query overrides options
      if (options.query)
          Object.keys(options.query).forEach(function(key) {
              if (key !== "query")
                  switch (options[key] = options.query[key]) {
                      case "true":
                          options[key] = true;
                          break;
                      case "false":
                          options[key] = false;
                          break;
                      case "null":
                          options[key] = null;
                          break;
                  }
          });
  
      // remove undocumented
      taffy({ undocumented: true }).remove();
      taffy({ ignore: true }).remove();
      taffy({ inherited: true }).remove();
  
      // remove private
      if (!options.private)
          taffy({ access: "private" }).remove();
  
      // setup output
      out = options.destination
          ? fs.createWriteStream(options.destination)
          : process.stdout;
  
      try {
          // setup environment
          data = taffy().get();
          indent = 0;
          indentWritten = false;
          firstLine = true;
  
          // wrap everything in a module if configured
          if (options.module) {
              writeln("export = ", options.module, ";");
              writeln();
              writeln("declare namespace ", options.module, " {");
              writeln();
              ++indent;
          }
  
          // handle all
          getChildrenOf(undefined).forEach(function(child) {
              handleElement(child, null);
          });
  
          // process queued
          while (queuedInterfaces.length) {
              var element = queuedInterfaces.shift();
              begin(element);
              writeInterface(element);
              writeln(";");
          }
  
          // end wrap
          if (options.module) {
              --indent;
              writeln("}");
          }
  
          // close file output
          if (out !== process.stdout)
              out.end();
  
      } finally {
          // gc environment objects
          out = data = null;
          seen = options = {};
          queuedInterfaces = [];
      }
  };
  
  //
  // Utility
  //
  
  // writes one or multiple strings
  function write() {
      var s = Array.prototype.slice.call(arguments).join("");
      if (!indentWritten) {
          for (var i = 0; i < indent; ++i)
              s = "    " + s;
          indentWritten = true;
      }
      out.write(s);
      firstLine = false;
  }
  
  // writes zero or multiple strings, followed by a new line
  function writeln() {
      var s = Array.prototype.slice.call(arguments).join("");
      if (s.length)
          write(s, "\n");
      else if (!firstLine)
          out.write("\n");
      indentWritten = false;
  }
  
  var keepTags = [
      "param",
      "returns",
      "throws",
      "see"
  ];
  
  // parses a comment into text and tags
  function parseComment(comment) {
      var lines = comment.replace(/^ *\/\*\* *|^ *\*\/| *\*\/ *$|^ *\* */mg, "").trim().split(/\r?\n|\r/g); // property.description has just "\r" ?!
      var desc;
      var text = [];
      var tags = null;
      for (var i = 0; i < lines.length; ++i) {
          var match = /^@(\w+)\b/.exec(lines[i]);
          if (match) {
              if (!tags) {
                  tags = [];
                  desc = text;
              }
              text = [];
              tags.push({ name: match[1], text: text });
              lines[i] = lines[i].substring(match[1].length + 1).trim();
          }
          if (lines[i].length || text.length)
              text.push(lines[i]);
      }
      return {
          text: desc || text,
          tags: tags || []
      };
  }
  
  // writes a comment
  function writeComment(comment, otherwiseNewline) {
      if (!comment || options.comments === false) {
          if (otherwiseNewline)
              writeln();
          return;
      }
      if (typeof comment !== "object")
          comment = parseComment(comment);
      comment.tags = comment.tags.filter(function(tag) {
          return keepTags.indexOf(tag.name) > -1 && (tag.name !== "returns" || tag.text[0] !== "{undefined}");
      });
      writeln();
      if (!comment.tags.length && comment.text.length < 2) {
          writeln("/** " + comment.text[0] + " */");
          return;
      }
      writeln("/**");
      comment.text.forEach(function(line) {
          if (line.length)
              writeln(" * ", line);
          else
              writeln(" *");
      });
      comment.tags.forEach(function(tag) {
          var started = false;
          if (tag.text.length) {
              tag.text.forEach(function(line, i) {
                  if (i > 0)
                      write(" * ");
                  else if (tag.name !== "throws")
                      line = line.replace(/^\{[^\s]*} ?/, "");
                  if (!line.length)
                      return;
                  if (!started) {
                      write(" * @", tag.name, " ");
                      started = true;
                  }
                  writeln(line);
              });
          }
      });
      writeln(" */");
  }
  
  // recursively replaces all occurencies of re's match
  function replaceRecursive(name, re, fn) {
      var found;
  
      function replacer() {
          found = true;
          return fn.apply(null, arguments);
      }
  
      do {
          found = false;
          name = name.replace(re, replacer);
      } while (found);
      return name;
  }
  
  // tests if an element is considered to be a class or class-like
  function isClassLike(element) {
      return isClass(element) || isInterface(element);
  }
  
  // tests if an element is considered to be a class
  function isClass(element) {
      return element && element.kind === "class";
  }
  
  // tests if an element is considered to be an interface
  function isInterface(element) {
      return element && (element.kind === "interface" || element.kind === "mixin");
  }
  
  // tests if an element is considered to be a namespace
  function isNamespace(element) {
      return element && (element.kind === "namespace" || element.kind === "module");
  }
  
  // gets all children of the specified parent
  function getChildrenOf(parent) {
      var memberof = parent ? parent.longname : undefined;
      return data.filter(function(element) {
          return element.memberof === memberof;
      });
  }
  
  // gets the literal type of an element
  function getTypeOf(element) {
      if (element.tsType)
          return element.tsType.replace(/\r?\n|\r/g, "\n");
      var name = "any";
      var type = element.type;
      if (type && type.names && type.names.length) {
          if (type.names.length === 1)
              name = element.type.names[0].trim();
          else
              name = "(" + element.type.names.join("|") + ")";
      } else
          return name;
  
      // Replace catchalls with any
      name = name.replace(/\*|\bmixed\b/g, "any");
  
      // Ensure upper case Object for map expressions below
      name = name.replace(/\bobject\b/g, "Object");
  
      // Correct Something.<Something> to Something<Something>
      name = replaceRecursive(name, /\b(?!Object|Array)([\w$]+)\.<([^>]*)>/gi, function($0, $1, $2) {
          return $1 + "<" + $2 + ">";
      });
  
      // Replace Array.<string> with string[]
      name = replaceRecursive(name, /\bArray\.?<([^>]*)>/gi, function($0, $1) {
          return $1 + "[]";
      });
  
      // Replace Object.<string,number> with { [k: string]: number }
      name = replaceRecursive(name, /\bObject\.?<([^,]*), *([^>]*)>/gi, function($0, $1, $2) {
          return "{ [k: " + $1 + "]: " + $2 + " }";
      });
  
      // Replace functions (there are no signatures) with Function
      name = name.replace(/\bfunction(?:\(\))?\b/g, "Function");
  
      // Convert plain Object back to just object
      name = name.replace(/\b(Object\b(?!\.))/g, function($0, $1) {
          return $1.toLowerCase();
      });
  
      return name;
  }
  
  // begins writing the definition of the specified element
  function begin(element, is_interface) {
      if (!seen[element.longname]) {
          if (isClass(element)) {
              var comment = parseComment(element.comment);
              var classdesc = comment.tags.find(function(tag) { return tag.name === "classdesc"; });
              if (classdesc) {
                  comment.text = classdesc.text;
                  comment.tags = [];
              }
              writeComment(comment, true);
          } else
              writeComment(element.comment, is_interface || isClassLike(element) || isNamespace(element) || element.isEnum || element.scope === "global");
          seen[element.longname] = element;
      } else
          writeln();
      if (element.scope !== "global" || options.module)
          return;
      write("export ");
  }
  
  // writes the function signature describing element
  function writeFunctionSignature(element, isConstructor, isTypeDef) {
      write("(");
  
      var params = {};
  
      // this type
      if (element.this)
          params["this"] = {
              type: element.this.replace(/^{|}$/g, ""),
              optional: false
          };
  
      // parameter types
      if (element.params)
          element.params.forEach(function(param) {
              var path = param.name.split(/\./g);
              if (path.length === 1)
                  params[param.name] = {
                      type: getTypeOf(param),
                      variable: param.variable === true,
                      optional: param.optional === true,
                      defaultValue: param.defaultvalue // Not used yet (TODO)
                  };
              else // Property syntax (TODO)
                  params[path[0]].type = "{ [k: string]: any }";
          });
  
      var paramNames = Object.keys(params);
      paramNames.forEach(function(name, i) {
          var param = params[name];
          var type = param.type;
          if (param.variable) {
              name = "..." + name;
              type = param.type.charAt(0) === "(" ? "any[]" : param.type + "[]";
          }
          write(name, !param.variable && param.optional ? "?: " : ": ", type);
          if (i < paramNames.length - 1)
              write(", ");
      });
  
      write(")");
  
      // return type
      if (!isConstructor) {
          write(isTypeDef ? " => " : ": ");
          var typeName;
          if (element.returns && element.returns.length && (typeName = getTypeOf(element.returns[0])) !== "undefined")
              write(typeName);
          else
              write("void");
      }
  }
  
  // writes (a typedef as) an interface
  function writeInterface(element) {
      write("interface ", element.name);
      writeInterfaceBody(element);
      writeln();
  }
  
  function writeInterfaceBody(element) {
      writeln("{");
      ++indent;
      if (element.tsType)
          writeln(element.tsType.replace(/\r?\n|\r/g, "\n"));
      else if (element.properties && element.properties.length)
          element.properties.forEach(writeProperty);
      --indent;
      write("}");
  }
  
  function writeProperty(property, declare) {
      writeComment(property.description);
      if (declare)
          write("");
      write(property.name);
      if (property.optional)
          write("?");
      writeln(": ", getTypeOf(property), ";");
  }
  
  //
  // Handlers
  //
  
  // handles a single element of any understood type
  function handleElement(element, parent) {
      if (element.scope === "inner")
          return false;
  
      if (element.optional !== true && element.type && element.type.names && element.type.names.length) {
          for (var i = 0; i < element.type.names.length; i++) {
              if (element.type.names[i].toLowerCase() === "undefined") {
                  // This element is actually optional. Set optional to true and
                  // remove the 'undefined' type
                  element.optional = true;
                  element.type.names.splice(i, 1);
                  i--;
              }
          }
      }
  
      if (seen[element.longname])
          return true;
      if (isClassLike(element))
          handleClass(element, parent);
      else switch (element.kind) {
          case "module":
          case "namespace":
              handleNamespace(element, parent);
              break;
          case "constant":
          case "member":
              handleMember(element, parent);
              break;
          case "function":
              handleFunction(element, parent);
              break;
          case "typedef":
              handleTypeDef(element, parent);
              break;
          case "package":
              break;
      }
      seen[element.longname] = element;
      return true;
  }
  
  // handles (just) a namespace
  function handleNamespace(element/*, parent*/) {
      var children = getChildrenOf(element);
      if (!children.length)
          return;
      var first = true;
      if (element.properties)
          element.properties.forEach(function(property) {
              if (!/^[$\w]+$/.test(property.name)) // incompatible in namespace
                  return;
              if (first) {
                  begin(element);
                  writeln("namespace ", element.name, " {");
                  ++indent;
                  first = false;
              }
              writeProperty(property, true);
          });
      children.forEach(function(child) {
          if (child.scope === "inner" || seen[child.longname])
              return;
          if (first) {
              begin(element);
              writeln("namespace ", element.name, " {");
              ++indent;
              first = false;
          }
          handleElement(child, element);
      });
      if (!first) {
          --indent;
          writeln("}");
      }
  }
  
  // a filter function to remove any module references
  function notAModuleReference(ref) {
      return ref.indexOf("module:") === -1;
  }
  
  // handles a class or class-like
  function handleClass(element, parent) {
      var is_interface = isInterface(element);
      begin(element, is_interface);
      if (is_interface)
          write("interface ");
      else {
          if (element.virtual)
              write("abstract ");
          write("class ");
      }
      write(element.name);
      if (element.templates && element.templates.length)
          write("<", element.templates.join(", "), ">");
      write(" ");
  
      // extended classes
      if (element.augments) {
          var augments = element.augments.filter(notAModuleReference);
          if (augments.length)
              write("extends ", augments[0], " ");
      }
  
      // implemented interfaces
      var impls = [];
      if (element.implements)
          Array.prototype.push.apply(impls, element.implements);
      if (element.mixes)
          Array.prototype.push.apply(impls, element.mixes);
      impls = impls.filter(notAModuleReference);
      if (impls.length)
          write("implements ", impls.join(", "), " ");
  
      writeln("{");
      ++indent;
  
      if (element.tsType)
          writeln(element.tsType.replace(/\r?\n|\r/g, "\n"));
  
      // constructor
      if (!is_interface && !element.virtual)
          handleFunction(element, parent, true);
  
      // properties
      if (is_interface && element.properties)
          element.properties.forEach(function(property) {
              writeProperty(property);
          });
  
      // class-compatible members
      var incompatible = [];
      getChildrenOf(element).forEach(function(child) {
          if (isClassLike(child) || child.kind === "module" || child.kind === "typedef" || child.isEnum) {
              incompatible.push(child);
              return;
          }
          handleElement(child, element);
      });
  
      --indent;
      writeln("}");
  
      // class-incompatible members
      if (incompatible.length) {
          writeln();
          if (element.scope === "global" && !options.module)
              write("export ");
          writeln("namespace ", element.name, " {");
          ++indent;
          incompatible.forEach(function(child) {
              handleElement(child, element);
          });
          --indent;
          writeln("}");
      }
  }
  
  // handles a namespace or class member
  function handleMember(element, parent) {
      begin(element);
  
      if (element.isEnum) {
          var stringEnum = false;
          element.properties.forEach(function(property) {
              if (isNaN(property.defaultvalue)) {
                  stringEnum = true;
              }
          });
          if (stringEnum) {
              writeln("type ", element.name, " =");
              ++indent;
              element.properties.forEach(function(property, i) {
                  write(i === 0 ? "" : "| ", JSON.stringify(property.defaultvalue));
              });
              --indent;
              writeln(";");
          } else {
              writeln("enum ", element.name, " {");
              ++indent;
              element.properties.forEach(function(property, i) {
                  write(property.name);
                  if (property.defaultvalue !== undefined)
                      write(" = ", JSON.stringify(property.defaultvalue));
                  if (i < element.properties.length - 1)
                      writeln(",");
                  else
                      writeln();
              });
              --indent;
              writeln("}");
          }
  
      } else {
  
          var inClass = isClassLike(parent);
          if (inClass) {
              write(element.access || "public", " ");
              if (element.scope === "static")
                  write("static ");
              if (element.readonly)
                  write("readonly ");
          } else
              write(element.kind === "constant" ? "const " : "let ");
  
          write(element.name);
          if (element.optional)
              write("?");
          write(": ");
  
          if (element.type && element.type.names && /^Object\b/i.test(element.type.names[0]) && element.properties) {
              writeln("{");
              ++indent;
              element.properties.forEach(function(property, i) {
                  writeln(JSON.stringify(property.name), ": ", getTypeOf(property), i < element.properties.length - 1 ? "," : "");
              });
              --indent;
              writeln("};");
          } else
              writeln(getTypeOf(element), ";");
      }
  }
  
  // handles a function or method
  function handleFunction(element, parent, isConstructor) {
      var insideClass = true;
      if (isConstructor) {
          writeComment(element.comment);
          write("constructor");
      } else {
          begin(element);
          insideClass = isClassLike(parent);
          if (insideClass) {
              if (!element.virtual) {
                  write(element.access || "public", " ");
              }
              if (element.scope === "static")
                  write("static ");
          } else
              write("function ");
          write(element.name);
          if (element.templates && element.templates.length)
              write("<", element.templates.join(", "), ">");
      }
      writeFunctionSignature(element, isConstructor, false);
      writeln(";");
      if (!insideClass)
          handleNamespace(element);
  }
  
  // handles a type definition (not a real type)
  function handleTypeDef(element, parent) {
      if (isInterface(element)) {
          if (isClassLike(parent))
              queuedInterfaces.push(element);
          else {
              begin(element);
              writeInterface(element);
          }
      } else {
          writeComment(element.comment, true);
          write("type ", element.name);
          if (element.templates && element.templates.length)
              write("<", element.templates.join(", "), ">");
          write(" = ");
          if (element.tsType)
              write(element.tsType.replace(/\r?\n|\r/g, "\n"));
          else {
              var type = getTypeOf(element);
              if (element.type && element.type.names.length === 1 && element.type.names[0] === "function")
                  writeFunctionSignature(element, false, true);
              else if (type === "object") {
                  if (element.properties && element.properties.length)
                      writeInterfaceBody(element);
                  else
                      write("{}");
              } else
                  write(type);
          }
          writeln(";");
      }
  }
  