// Returns a new Builder-compatible object that works just like a

// Builder, but interprets all paths relative to 'relPath', a path

// The sub-builder returned does not have all Builder methods (for

// example, complete() wouldn't make sense) and you should not rely

keep keep keep keep keep replace keep keep keep

// relative to the bundle root which should not start with a '/'.

// == Site Archive (*.star) file layout (subject to rapid change) ==
//
// /star.json
//
//  - version: "1" for this format
//
//  - builtBy: human readable banner (eg, "Meteor 0.6.0")
//
//  - programs: array of programs in the star, each an object:
//    - name: short, unique name for program, for referring to it
//      programmatically
//    - arch: architecture that this program targets. Currently it is
//            "client" or "server" but in the future this will change
//            to something like "browser.w3c" or "darwin.x86_64".
//    - path: directory (relative to star.json) containing this program
//
//    XXX in the future this will also contain instructions for
//    mounting packages into the namespace of each program, and
//    possibly for mounting programs on top of each other (this would
//    be the principled mechanism by which a server program could read
//    a client program so it can server it)
//
// /README: human readable instructions
//
// /main.js: script that can be run in node.js to start the site
//   running in standalone mode (after setting appropriate environment
//   variables as documented in README)
//
// /server/.bundle_version.txt: contains the dev_bundle version that
//   legacy (read: current) Galaxy version read in order to set
//   NODE_PATH to point to arch-specific builds of binary node modules
//   (primarily this is for node-fibers)
//
// XXX in the future one program (which must be a server-type
// architecture) will be designated as the 'init' program. The
// container will call it with arguments to signal app lifecycle
// events like 'start' and 'stop'.
//
//
// Conventionally programs will be located at /programs/<name>, but
// really the build tool can lay out the star however it wants.
//
//
// == Format of a program when arch is "client" ==
//
// Standard:
//
// /program.json
//
//  - version: "1" for this format
//
//  - page: path to the template for the HTML to serve when a browser
//    loads a page that is part of the application. In the file
//    ##HTML_ATTRIBUTES## and ##RUNTIME_CONFIG## will be replaced with
//    appropriate values at runtime.
//
//  - manifest: array of resources to serve with HTTP, each an object:
//    - path: path of file relative to program.json
//    - where: "client"
//    - type: "js", "css", or "static"
//    - cacheable: is it safe to ask the browser to cache this file (boolean)
//    - url: relative url to download the resource, includes cache busting
//        parameter when used
//    - size: size of file in bytes
//    - hash: sha1 hash of the file contents
//    Additionally there will be an entry with where equal to
//    "internal", path equal to page (above), and hash equal to the
//    sha1 of page (before replacements.) Currently this is used to
//    trigger HTML5 appcache reloads at the right time (if the
//    'appcache' package is being used.)
//
//  - static: a path, relative to program.json, to a directory. If the
//    server is too dumb to read 'manifest', it can just serve all of
//    the files in this directory (with a relatively short cache
//    expiry time.)
//    XXX do not use this. It will go away soon.
//
//  - static_cacheable: just like 'static' but resources that can be
//    cached aggressively (cacheable: true in the manifest)
//    XXX do not use this. It will go away soon.
//
// Convention:
//
// page is 'app.html', static is 'static', and staticCacheable is
// 'static_cacheable'.
//
//
// == Format of a program when arch is "server" ==
//
// Standard:
//
// /server.js: script to run inside node.js to start the program
//
// XXX Subject to change! This will likely change to a shell script
// (allowing us to represent types of programs that don't use or
// depend on node) -- or in fact, rather than have anything fixed here
// at all, star.json just contains a path to a program to run, which
// can be anything than can be exec()'d.
//
// Convention:
//
// /program.json:
//
//  - load: array with each item describing a JS file to load at startup:
//    - path: path of file, relative to program.json
//    - node_modules: if Npm.require is called from this file, this is
//      the path (relative to program.json) of the directory that should
//      be search for npm modules
//
//  - client: the client program that should be served up by HTTP,
//    expressed as a path (relative to program.json) to the *client's*
//    program.json.
//
//  - config: additional framework-specific configuration. currently:
//    - meteorRelease: the value to use for Meteor.release, if any
//
// /app/*: source code of the (server part of the) app
// /packages/foo.js: the (linked) source code for package foo
// /package-tests/foo.js: the (linked) source code for foo's tests
// /npm/foo/bar: node_modules for slice bar of package foo. may be
// symlinked if developing locally.
//
// /node_modules: node_modules needed for server.js. omitted if
// deploying (see .bundle_version.txt above), copied if bundling,
// symlinked if developing locally.

var path = require('path');
var files = require(path.join(__dirname, 'files.js'));
var packages = require(path.join(__dirname, 'packages.js'));
var linker = require(path.join(__dirname, 'linker.js'));
var Builder = require(path.join(__dirname, 'builder.js'));
var fs = require('fs');
var uglify = require('uglify-js');
var cleanCSS = require('clean-css');
var _ = require('underscore');
var project = require(path.join(__dirname, 'project.js'));
var builder = require(path.join(__dirname, 'builder.js'));

// files to ignore when bundling. node has no globs, so use regexps
var ignoreFiles = [
    /~$/, /^\.#/, /^#.*#$/,
    /^\.DS_Store$/, /^ehthumbs\.db$/, /^Icon.$/, /^Thumbs\.db$/,
    /^\.meteor$/, /* avoids scanning N^2 files when bundling all packages */
    /^\.git$/ /* often has too many files to watch */
];

// http://davidshariff.com/blog/javascript-inheritance-patterns/
var inherits = function (child, parent) {
  var tmp = function () {};
  tmp.prototype = parent.prototype;
  child.prototype = new tmp;
  child.prototype.constructor = child;
};

///////////////////////////////////////////////////////////////////////////////
// NodeModulesDirectory
///////////////////////////////////////////////////////////////////////////////

// Represents a node_modules directory that we need to copy into the
// bundle or otherwise make available at runtime.
var NodeModulesDirectory = function (options) {
  var self = this;

  // The absolute path (on local disk at build time) to a directory
  // that contains the built node_modules to use.
  self.sourcePath = options.sourcePath;

  // The path (relative to the bundle root) where we would preferably
  // like the node_modules to be output (essentially cosmetic.)
  self.preferredBundlePath = options.bundlePath;
};

///////////////////////////////////////////////////////////////////////////////
// File
///////////////////////////////////////////////////////////////////////////////

// Allowed options:
// - data: contents of the file as a Buffer
// - cacheable
var File = function (options) {
  var self = this;

  if (options.data && ! (options.data instanceof Buffer)) {
    throw new Error('File contents must be provided as a Buffer');
  }

  // The absolute path in the filesystem from which we loaded (or will
  // load) this file (null if the file does not correspond to one on
  // disk.)
  self.sourcePath = null;

  // Where this file is intended to reside within the target's
  // filesystem.
  self.targetPath = null;

  // The URL at which this file is intended to be served, relative to
  // the base URL at which the target is being served (ignored if this
  // file is not intended to be served over HTTP.)
  self.url = null

  // Is this file guaranteed to never change, so that we can let it be
  // cached forever? Only makes sense of self.url is set.
  self.cacheable = options.cacheable || false;

  // The node_modules directory that Npm.require() should search when
  // called from inside this file, given as a NodeModulesDirectory, or
  // null if Npm.depend() is not in effect for this file. Only works
  // in the "server" architecture.
  self.nodeModulesDirectory = null;

  self._contents = options.data || null; // contents, if known, as a Buffer
  self._hash = null; // hash, if known, as a hex string
};

_.extend(File.prototype, {
  hash: function () {
    var self = this;
    if (! self._hash)
      self._hash = Builder.sha1(self.contents());
    return self._hash;
  },

  // Omit encoding to get a buffer, or provide something like 'utf8'
  // to get a string
  contents: function (encoding) {
    var self = this;
    if (! self._contents) {
      if (! self.sourcePath)
        throw new Error("Have neither contents nor sourcePath for file");
      else
        self._contents = fs.readFileSync(self.sourcePath);
    }

    return encoding ? self._contents.toString(encoding) : self._contents;
  },

  size: function () {
    var self = this;
    return self.contents().length;
  },

  // Set the URL of this file to "/<hash><suffix>". suffix will
  // typically be used to pick a reasonable extension. Also set
  // cacheable to true, since the file's name is now derived from its
  // contents.
  setUrlToHash: function (suffix) {
    var self = this;
    self.url = "/" + self.hash() + suffix;
    self.cacheable = true;
  },

  // Append "?<hash>" to the URL and mark the file as cacheable.
  addCacheBuster: function () {
    var self = this;
    if (! self.url)
      throw new Error("File must have a URL");
    if (self.cacheable)
      return; // eg, already got setUrlToHash
    if (/\?/.test(self.url))
      throw new Error("URL already has a query string");
    self.url += "?" + self.hash();
    self.cacheable = true;
  },

  // Given a relative path like 'a/b/c' (where '/' is this system's
  // path component separator), produce a URL that always starts with
  // a forward slash and that uses a literal forward slash as the
  // component separator.
  setUrlFromRelPath: function (relPath) {
    var self = this
    var url = relPath.split(path.sep).join('/');

    if (url.charAt(0) !== '/')
      url = '/' + url;

    self.url = url;
  }
});

///////////////////////////////////////////////////////////////////////////////
// Target
///////////////////////////////////////////////////////////////////////////////

// options:
// - arch: the architecture to build
//
// see subclasses for additional options
var Target = function (name, options) {
  var self = this;

  // Package library to use for resolving package dependenices.
  self.library = options.library;

  // A name for this target.
  self.name = name;

  // Path of this target in the bundle, relative to the root of the bundle.
  self.pathInBundle = path.join('programs', self.name);

  // Should be "client" or "server" for now, but soon we will get rid
  // of that and instead have something more like "browser.w3c" or
  // "nodejs.linux.i686".
  self.arch = options.arch;

  // All of the Slices that are to go into this target, in the order
  // that they are to be loaded.
  self.slices = [];

  // JavaScript files. List of File. They will be loaded at startup in
  // the order given.
  self.js = [];

  // Files and paths used by this target, in the format used by
  // watch.Watcher.
  self.dependencyInfo = {files: {}, directories: {}};

  // node_modules directories that we need to copy into the target (or
  // otherwise make available at runtime.) A map from an absolute path
  // on disk (NodeModulesDirectory.sourcePath) to a
  // NodeModulesDirectory object that we have created to represent it.
  self.nodeModulesDirectories = {};
};

_.extend(Target.prototype, {
  // Determine the packages to load, create Slices for
  // them, put them in load order, save in slices.
  //
  // options include:
  // - packages: an array of packages (or, properly speaking, slices)
  //   to include. Each element should either be a Package object or a
  //   package name as a string (to include that package's default
  //   slices for this arch, or a string of the form 'package.slice'
  //   to include a particular named slice from a particular package.
  // - test: an array of packages (as Package objects or as name
  //   strings) whose test slices should be included
  determineLoadOrder: function (options) {
    var self = this;
    var library = self.library;

    // Each of these are map from slice.id to Slice
    var needed = {}; // Slices that we still need to add
    var done = {}; // Slices that are already in self.slices
    var onStack = {}; // Slices that we're in the process of adding

    // Find the roots
    var rootSlices =
      _.flatten([
        _.map(options.packages || [], function (p) {
          if (typeof p === "string")
            return library.getSlices(p, self.arch);
          else
            return pkg.getDefaultSlices(self.arch);
        }),
        _.map(options.test || [], function (p) {
          var pkg = (p === "string" ? library.get(p) : p);
          return p.getTestSlices(self.arch);
        })
      ]);
    _.each(rootSlices, function (slice) {
      needed[slice.id] = slice;
    });

    // Set self.slices to be all of the roots, plus all of their
    // dependencies, in the correct load order. "Load order" means
    // that if X depends on (uses) Y, and that relationship is not
    // marked as unordered, Y appears before X in the ordering. Raises
    // an exception iff there is no such ordering (due to circular
    // dependency.)
    while (true) {
      // Get an arbitrary slice from those that remain, or break if
      // none remain
      var first = null;
      for (first in needed) break;
      if (! first)
        break;
      first = needed[first];

      // Add its strong dependencies to the order, then add it. Add
      // its weak dependencies to the list of things to add later.
      var add = function (slice) {
        if (done[slice.id])
          return;

        _.each(slice.uses, function (u) {
          _.each(library.getSlices(u.spec, self.arch), function (usedSlice) {
            if (u.unordered) {
              needed[usedSlice.id] = usedSlice;
              return;
            }
            if (onStack[usedSlice.id]) {
              console.error("fatal: circular dependency between packages " +
                            slice.pkg.name + " and " + usedSlice.pkg.name);
              process.exit(1);
            }
            onStack[usedSlice.id] = true;
            add(usedSlice);
            delete onStack[usedSlice.id];
          });
        });
        self.slices.push(slice);
        done[slice.id] = true;
        delete needed[slice.id];
      };
      add(first);
    }
  },

  // Sort the slices in dependency order, then, slice by slice, write
  // their resources into the bundle (which includes running the
  // JavaScript linker.)
  emitResources: function () {
    var self = this;

    // Copy their resources into the bundle in order
    _.each(self.slices, function (slice) {
      var isApp = ! slice.pkg.name;

      // Emit the resources
      _.each(slice.getResources(), function (resource) {
        if (_.contains(["js", "css", "static"], resource.type)) {
          if (resource.type === "css" && self.arch !== "client")
            // XXX might be nice to throw an error here, but then we'd
            // have to make it so that packages.js ignores css files
            // that appear in the server directories in an app tree

            // XXX XXX can't we easily do that in the css handler in
            // meteor.js?
            return;

          var f = new File({
            data: resource.data,
            cacheable: false
          });

          if (self.arch === "client")
            f.setUrlFromRelPath(resource.servePath);
          else {
            // XXX hack
            if (resource.servePath.match(/^\/packages\//))
              f.targetPath = resource.servePath;
            else
              f.targetPath = path.join('/app', resource.servePath);
          }

          if (self.arch === "server" && resource.type === "js" && ! isApp &&
              slice.nodeModulesPath) {
            var nmd = self.nodeModulesDirectories[slice.nodeModulesPath];
            if (! nmd) {
              nmd = new NodeModulesDirectory({
                sourcePath: slice.nodeModulesPath,
                preferredBundlePath: path.join('/npm', slice.pkg.name,
                                               slice.sliceName)
              });
              self.nodeModulesDirectories[slice.nodeModulesPath] = nmd;
            }
            f.nodeModulesDirectory = nmd;
          }

          self[resource.type].push(f);
          return;
        }

        if (_.contains(["head", "body"], resource.type)) {
          if (self.arch !== "client")
            throw new Error("HTML segments can only go to the client");
          self[resource.type].push(resource.data);
          return;
        }

        throw new Error("Unknown type " + resource.type);
      });

      // Depend on the source files that produced these
      // resources. (Since the dependencyInfo.directories should be
      // disjoint, it should be OK to merge them this way.)
      _.extend(self.dependencyInfo.files,
               slice.dependencyInfo.files);
      _.extend(self.dependencyInfo.directories,
               slice.dependencyInfo.directories);
    });
  },

  // Minify the JS in this target
  minifyJs: function () {
    var self = this;

    var allJs = _.map(self.js, function (file) {
      return file.contents('utf8');
    }).join('\n;\n');

    allJs = uglify.minify(allJs, {
      fromString: true,
      compress: {drop_debugger: false}
    }).code;

    self.js = [new File({ data: new Buffer(allJs, 'utf8') })];
    self.js[0].setUrlToHash(".js");
  },

  // For each resource of the given type, make it cacheable by adding
  // a query string to the URL based on its hash.
  addCacheBusters: function (type) {
    var self = this;
    _.each(self[type], function (file) {
      file.addCacheBuster();
    });
  },

  // Return all dependency info for this target, in the format
  // expected by watch.Watcher.
  getDependencyInfo: function () {
    var self = this;
    return self.dependencyInfo;
  }

});

//////////////////// ClientTarget ////////////////////

var ClientTarget = function (name, options) {
  var self = this;
  Target.apply(this, arguments);

  // CSS files. List of File. Applicable only on 'client'
  // architecture. They will be loaded at page load in the order
  // given.
  self.css = [];

  // Static assets to serve with HTTP. List of File. Applicable only
  // on 'client' architecture.
  self.static = [];

  // List of segments of additional HTML for <head>/<body>. Only for
  // "client" arch.
  self.head = [];
  self.body = [];
};

inherits(ClientTarget, Target);

_.extend(ClientTarget.prototype, {
  // Minify the JS in this target
  minifyCss: function () {
    var self = this;

    var allCss = _.map(self.css, function (file) {
      return file.contents('utf8');
    }).join('\n');

    allCss = cleanCSS.process(allCss);

    self.css = [new File({ data: new Buffer(allCss, 'utf8') })];
    self.css[0].setUrlToHash(".css");
  },

  // Add all of the files in a directory `rootDir` (and its
  // subdirectories) as static assets. `rootDir` should be an absolute
  // path. Only makes sense on clients. If provided, exclude is an
  // array of filename regexps to exclude. If provided, assetPath is a
  // prefix to use when computing the path for each file in the
  // client's asset tree.
  addAssetDir: function (rootDir, exclude, assetPathPrefix) {
    var self = this;
    exclude = exclude || [];

    if (self.arch !== "client")
      throw new Error("Only clients can have assets");

    self.dependencyInfo.directories[dir] = {
      include: [/.?/],
      exclude: exclude
    };

    var walk = function (dir, assetPath) {
      _.each(fs.readdirSync(dir), function (item) {
        // Skip excluded files
        var matchesAnExclude = _.any(exclude, function (pattern) {
          return item.match(pattern);
        });
        if (matchesAnExclude)
          return;

        var absPath = path.resolve(dir, item);
        assetPath = path.join(dir, item);
        if (fs.statSync(absPath).isDirectory()) {
          walk(absPath, assetPath);
          return;
        }

        var f = new File({ sourcePath: absPath });
        f.setUrlFromRelPath(assetPath);
        self.dependencyInfo.files[absPath] = f.hash();
        self.static.push(f);
      });
    };

    walk(rootDir, assetPathPrefix || '');
  },

  assignTargetPaths: function () {
    var self = this;
    _.each(["js", "css", "static"], function (type) {
      _.each(self[type], function (file) {
        if (! file.targetPath) {
          if (! file.url)
            throw new Error("Client file with no URL?");

          var parts = file.url.replace(/\?.*$/, '').split('/').slice(1);
          parts.unshift(file.cacheable ? "static_cacheable" : "static");
          file.targetPath = path.sep + path.join.apply(path, parts);
        }
      });
    });
  },

  generateHtmlBoilerplate: function () {
    var self = this;

    var templatePath = path.join(__dirname, "app.html.in");
    var template = fs.readFileSync(templatePath);
    self.dependencyInfo.files[templatePath] = Builder.sha1(template);

    var f = require('handlebars').compile(template.toString());
    return new Buffer(f({
      scripts: _.pluck(self.js, 'url'),
      stylesheets: _.pluck(self.css, 'url'),
      head_extra: self.head.join('\n'),
      body_extra: self.body.join('\n')
    }), 'utf8');
  },

  // Output the finished target to disk
  write: function (builder) {
    var self = this;
    var manifest = [];

    // Resources served via HTTP
    _.each(["js", "css", "static"], function (type) {
      _.each(self[type], function (file) {

        if (! file.targetPath)
          throw new Error("No targetPath?");

        // XXX should probably use sanitize: true, but that will have
        // to wait until the server is actually driven by the manifest
        // (rather than just serving all of the files in a certain
        // directories)
        var contents = file.contents();
        if (! (contents instanceof Buffer))
          throw new Error("contents not a Buffer");
        builder.write(file.targetPath, { data: file.contents() });

        manifest.push({
          path: file.targetPath,
          where: "client",
          type: type,
          cacheable: file.cacheable,
          url: file.url,
          size: file.size(),
          hash: file.hash()
        });
      });
    });

    // HTML boilerplate (the HTML served to make the client load the
    // JS and CSS files and start the app)
    var htmlBoilerplate = self.generateHtmlBoilerplate();
    builder.write('app.html', { data: htmlBoilerplate });
    manifest.push({
      path: 'app.html',
      where: 'internal',
      hash: Builder.sha1(htmlBoilerplate)
    });

    // Control file
    builder.writeJson('program.json', {
      version: "1",
      manifest: manifest,
      page: 'app.html',

      // XXX the following are for use by 'legacy' (read: current)
      // server.js implementations which aren't smart enough to read
      // the manifest and instead want all of the resources in a
      // directory together so they can just point gzippo at it. we
      // should remove this and make the server work from the
      // manifest.
      static: 'static',
      staticCacheable: 'static_cacheable'
    });
  }
});

//////////////////// ServerTarget ////////////////////

// options:
// - clientTarget: the ClientTarget to serve up over HTTP as our client
// - releaseStamp: the Meteor release name (for retrieval at runtime)
var ServerTarget = function (name, options) {
  var self = this;
  Target.apply(this, arguments);

  self.clientTarget = options.clientTarget;
  self.releaseStamp = options.releaseStamp;
};

inherits(ServerTarget, Target);

_.extend(ServerTarget.prototype, {
  // Output the finished target to disk
  write: function (builder, nodeModulesMode) {
    var self = this;

    var json = {
      load: [],
      client: path.join(path.relative(self.pathInBundle,
                                      self.clientTarget.pathInBundle),
                        'program.json'),
      config: {
        meteorRelease: self.releaseStamp && self.releaseStamp !== "none" ?
          self.releaseStamp : undefined
      }
    };

    // Finalize choice of paths for node_modules directories -- These
    // paths are no longer just "preferred"; they are the final paths
    // that we will use
    _.each(self.nodeModulesDirectories, function (nmd) {
      nmd.preferredBundlePath =
        builder.generateFilename(nmd.preferredBundlePath, { directory: true });
    });

    // JavaScript sources
    _.each(self.js, function (file) {
      if (! file.targetPath)
        throw new Error("No targetPath?");

      builder.write(file.targetPath, { data: file.contents() });

      json.load.push({
        path: file.targetPath,
        node_modules: file.nodeModulesDirectory ?
          file.nodeModulesDirectory.preferredBundlePath : undefined
      });
    });

    // Server driver
    var serverPath = path.join(__dirname, 'server');
    builder.copyDirectory({
      from: serverPath,
      to: '/',
      ignore: ignoreFiles
    });

    // Main, architecture-dependent node_modules from the dependency
    // kit. This one is copied in 'meteor bundle', symlinked in
    // 'meteor run', and omitted by 'meteor deploy' (Galaxy provides a
    // version that's appropriate for the server architecture.)
    if (nodeModulesMode !== "skip") {
      builder.copyDirectory({
        from: path.join(files.get_dev_bundle(), 'lib', 'node_modules'),
        to: 'node_modules',
        ignore: ignoreFiles,
        depend: false
      });
    }

    // Extra user-defined arch-independent node_modules. 'meteor
    // bundle' and 'meteor deploy' copy them, and 'meteor run'
    // symlinks them. (XXX Note that this doesn't work for
    // arch-specific packages. They'll just break if you deploy to a
    // different arch than you built on. We'll get to that Soon
    // Enough!)

    // XXX we should consider supporting bundle time-only npm
    // dependencies which don't need to be pushed to the server.

    _.each(self.nodeModulesDirectories, function (nmd) {
      builder.copyDirectory({
        from: nmd.sourcePath,
        to: nmd.preferredBundlePath,
        depend: false
      });
    });

    // Control file
    builder.writeJson('program.json', json);
  }
});

//////////////////// InProcessTarget ////////////////////

var InProcessTarget = function (name, options) {
  var self = this;
  Target.apply(this, arguments);
};

inherits(InProcessTarget, Target);

_.extend(InProcessTarget.prototype, {
  // Load all of the JavaScript in this target into the current process
  load: function (builder, nodeModulesMode) {
    var self = this;

    // Eval each JavaScript file, providing a 'Npm' symbol in the same
    // way that the server environment would
    _.each(self.js, function (file) {
      var Npm = {
        require: function (name) {
          if (! file.nodeModulesDirectory) {
            // No Npm.depends associated with this package
            return require(name);
          }

          var nodeModuleDir =
            path.join(file.nodeModulesDirectory.sourcePath, name);

          if (fs.existsSync(nodeModuleDir)) {
            return require(nodeModuleDir);
          }
          try {
            return require(name);
          } catch (e) {
            throw new Error("Can't load npm module '" + name +
                            "' while loading " + file.targetPath +
                            ". Check your Npm.depends().'");
          }
        }
      };
      // \n is necessary in case final line is a //-comment
      var wrapped = "(function(Npm){" +
        file.contents().toString('utf8') + "\n})";
      var func = require('vm').runInThisContext(wrapped, file.targetPath, true);
      func(Npm);
    });
  }
});

///////////////////////////////////////////////////////////////////////////////
// writeSiteArchive
///////////////////////////////////////////////////////////////////////////////

// targets is an array of Targets to include in the bundle. outputPath
// is the path of a directory that should be created to contain the
// generated site archive.
//
// Returns dependencyInfo (in the format expected by watch.Watcher)
// for all files and directories that ultimately went into the bundle.
//
// options:
// - nodeModulesMode: "skip", "symlink", "copy"
// - builtBy: vanity identification string to write into metadata
var writeSiteArchive = function (targets, outputPath, options) {
  var builder = new Builder({
    outputPath: outputPath,
    symlink: options.nodeModulesMode === "symlink"
  });

  try {
    var json = {
      version: "1",
      builtBy: options.builtBy,
      programs: []
    };

    // Write out each target
    _.each(targets, function (target) {
      target.pathInBundle = path.join('programs', target.name);
      target.write(builder.enter(target.pathInBundle), options.nodeModulesMode);
      json.programs.push({
        name: target.name,
        arch: target.arch,
        path: target.pathInBundle
      });
    });

    // Tell Galaxy what version of the dependency kit we're using, so
    // it can load the right modules. (Include this even if we copied
    // or symlinked a node_modules, since that's probably enough for
    // it to work in spite of the presence of node_modules for the
    // wrong arch.) The place we stash this is grody for temporary
    // reasons of backwards compatibility.
    builder.write(path.join('server', '.bundle_version.txt'), {
      file: path.join(files.get_dev_bundle(), '.bundle_version.txt')
    });

    // Affordances for standalone use
    var stub = new Buffer("require('./programs/server/server.js');\n", 'utf8');
    builder.write('main.js', { data: stub });

    builder.write('README', { data: new Buffer(
"This is a Meteor application bundle. It has only one dependency,\n" +
"node.js (with the 'fibers' package). To run the application:\n" +
"\n" +
"  $ npm install fibers@1.0.0\n" +
"  $ export MONGO_URL='mongodb://user:password@host:port/databasename'\n" +
"  $ export ROOT_URL='http://example.com'\n" +
"  $ export MAIL_URL='smtp://user:password@mailhost:port/'\n" +
"  $ node main.js\n" +
"\n" +
"Use the PORT environment variable to set the port where the\n" +
"application will listen. The default is 80, but that will require\n" +
"root on most systems.\n" +
"\n" +
"Find out more about Meteor at meteor.com.\n",
      'utf8')});

    // Control file
    builder.writeJson('star.json', json);

    // Merge the dependencyInfo of everything that went into the
    // bundle. A naive merge like this doesn't work in general but
    // should work in this case.
    var fileDeps = {}, directoryDeps = {};
    var dependencySources = targets.concat([builder]);
    _.each(dependencySources, function (s) {
      var info = s.getDependencyInfo();
      _.extend(fileDeps, info.files);
      _.extend(directoryDeps, info.directories);
    });

    // We did it!
    builder.complete();

    return {
      files: fileDeps,
      directories: directoryDeps
    };

  } catch (e) {
    builder.abort();
    throw e;
  }
};

///////////////////////////////////////////////////////////////////////////////
// Main
///////////////////////////////////////////////////////////////////////////////

/**
 * Take the Meteor app in projectDir, and compile it into a bundle at
 * outputPath. outputPath will be created if it doesn't exist (it
 * will be a directory), and removed if it does exist. The release
 * version is *not* read from the app's .meteor/release file. Instead,
 * it must be passed in as an option.
 *
 * Returns an object with keys:
 * - errors: An array of strings, or falsy if bundling succeeded.
 * - dependencyInfo: Information about files and paths that were
 *   inputs into the bundle and that we may wish to monitor for
 *   changes when developing interactively. It has two keys, 'files'
 *   and 'directories', in the format expected by watch.Watcher.
 *
 * On failure ('errors' is truthy), no bundle will be output (in fact,
 * outputPath will have been removed if it existed.)
 *
 * options include:
 * - minify : minify the CSS and JS assets
 *
 * - nodeModulesMode : decide on how to create the bundle's
 *   node_modules directory. one of:
 *     'skip' : don't create node_modules. used by `meteor deploy`, since
 *              our production servers already have all of the node modules
 *     'copy' : copy from a prebuilt local installation. used by
 *              `meteor bundle`
 *     'symlink' : symlink from a prebuild local installation. used
 *                 by `meteor run`
 *
 * - testPackages : array of package objects or package names whose
 *   tests should be included in this bundle
 *
 * - releaseStamp : The Meteor release version to use. This is *ONLY*
 *                  used as a stamp (eg Meteor.release). The package
 *                  search path is configured with 'library'.
 *
 * - library : Package library to use to fetch any required
 *   packages. NOTE: if there's an appDir here, it's used for package
 *   searching but it is NOT the appDir that we bundle!  So for
 *   "meteor test-packages" in an app, appDir is the test-runner-app
 *   but library.appDir is the app the user is in.
 */
exports.bundle = function (appDir, outputPath, options) {
  if (!options)
    throw new Error("Must pass options");
  if (!options.nodeModulesMode)
    throw new Error("Must pass options.nodeModulesMode");
  if (!options.library)
    throw new Error("Must pass options.library");
  if (!options.releaseStamp)
    throw new Error("Must pass options.releaseStamp or 'none'");

  var library = options.library;

  var builtBy = "Meteor" + (options.releaseStamp &&
                            options.releaseStamp !== "none" ?
                            " " + options.releaseStamp : "");

  try {
    // Create targets
    var client = new ClientTarget("client", {
      library: library,
      arch: "client"
    });
    var server = new ServerTarget("server", {
      library: library,
      arch: "server",
      clientTarget: client,
      releaseStamp: options.releaseStamp
    });
    var targets = [client, server];

    // Create a Package object that represents the app
    var app = library.getForApp(appDir, ignoreFiles);

    // Populate the list of slices to load
    client.determineLoadOrder({
      packages: [app],
      test: options.testPackages || []
    });
    server.determineLoadOrder({
      packages: [app],
      test: options.testPackages || []
    });

    // Link JavaScript, put resources in load order, and copy them to
    // the bundle
    client.emitResources();
    server.emitResources();

    // Minify, if requested (only the client)
    if (options.minify) {
      client.minifyJs();
      client.minifyCss();
    }

    // Add assets from /public directory
    // XXX this should probably be part of the appDir reader
    if (files.is_app_dir(appDir)) { /* XXX what is this checking? */
      var publicDir = path.join(appDir, 'public');
      if (fs.existsSync(publicDir))
        client.addAssetDir(publicDir, ignoreFiles);
    }

    // Make client-side CSS and JS assets cacheable forever, by adding
    // a query string with a cache-busting hash.
    client.addCacheBusters("js");
    client.addCacheBusters("css");

    // Write to disk
    client.assignTargetPaths();
    var dependencyInfo = writeSiteArchive(targets, outputPath, {
      nodeModulesMode: options.nodeModulesMode,
      builtBy: builtBy
    });

    return {
      errors: false,
      dependencyInfo: dependencyInfo
    };
  } catch (err) {
    return {
      errors: ["Exception while bundling application:\n" + (err.stack || err)]
    };
  }
};

// Public entry point is unipackage.load, but for now it shares a lot
// of innards with the bundler (this possibly will get factored out at
// some point) so the implementation is here
exports._load = function (library, packages) {
  var target = new InProcessTarget("load", {
    library: library,
    arch: "server"
  });

  target.determineLoadOrder({ packages: packages });
  target.emitResources();
  // I love it when a plan comes together
  target.load();
};


/******************************************************************************/

var create = function (id, start, end, inner, tag) {
  var ret = new LiveRange(tag || 'a', start, end, inner);
  ret.id = id;
  return ret;
};

var frag = function (html) {
  var ret = document.createDocumentFragment();
  var q = $(html);
  for (var i = 0; i < q.length; i++)
    ret.appendChild(q[i]);
  return ret;
};

// takes ranges or fragments. tag is used only for fragments.
var dump = function (what, tag) {
  var ret = "";

  var emit = function (isStart, obj) {
    ret += (isStart ? "<": "</") + obj.id + ">";
  };

  if (typeof what === 'object' && what.nodeType === 11 /* DocumentFragment */) {
    if (what.firstChild) {
      var range = new LiveRange(tag || 'a', what);
      range.visit(emit, emit);
      range.destroy();
    }
  } else {
    emit(true, what);
    what.visit(emit, emit);
    emit(false, what);
  }

  return ret;
};

// actual can be a range or a fragment
var assert_dump = function (test, expected, actual, tag) {
  test.equal(dump(actual), expected, "Tree doesn't match");
  if (actual instanceof LiveRange)
    check_liverange_integrity(actual);
  else {
    if (actual.firstChild) {
      var range = new LiveRange(tag || 'a', actual);
      check_liverange_integrity(range);
      range.destroy();
    }
  }
};

var contained_ranges = function (range) {
  var result = {range: range, children: []};
  var stack = [result];

  range.visit(function (isStart, range) {
    if (isStart) {
      var record = {range: range, children: []};
      stack[stack.length - 1].children.push(record);
      stack.push(record);
    } else
      if (stack.pop().range !== range)
        throw new Error("Overlapping ranges detected");
  });

  return result;
};

var assert_contained = function (r, expected) {
  // one day, fold in the above function (use visit() directly)
  var actual = contained_ranges(r);

  var traverse = function (exp, act) {
    if (exp.range !== act.range)
      throw new Error("contained(): range doesn't match");
    if (exp.children.length !== act.children.length)
      throw new Error("contained(): different tree shape");
    for (var i = 0; i < exp.children.length; i++)
      traverse(exp.children[i], act.children[i]);
  };

  traverse(expected, actual);
};

/******************************************************************************/

Tinytest.add("liverange - single node", function (test) {
  var f = frag("

");
  var r_a = create("a", f);
  test.instanceOf(r_a, LiveRange);
  assert_dump(test, "<1></1>", r_a);
  assert_dump(test, "<1></1>", f);
  assert_contained(r_a, {range: r_a, children: []});

  var r_b = create("b", f);
  assert_dump(test, "<1></1>", r_a);
  assert_dump(test, "<1></1>", r_b);
  assert_dump(test, "<1></1>", f);
  assert_contained(r_a, {range: r_a, children: []});
  assert_contained(r_b, {range: r_b, children: [{range: r_a, children: []}]});
  test.equal(r_a.firstNode(), f.firstChild);
  test.equal(r_a.lastNode(), f.lastChild);
  test.equal(r_b.firstNode(), f.firstChild);
  test.equal(r_b.lastNode(), f.lastChild);

  var ret1 = r_a.replaceContents(frag("

"), true);
  test.equal(ret1.nodeType, 11 /* DocumentFragment */);
  assert_dump(test, "<1></1>", ret1);
  assert_dump(test, "<2></2>", r_a);
  assert_dump(test, "<2></2>", r_b);
  assert_dump(test, "<2></2>", f);

  var ret2 = r_b.replaceContents(frag("

"), true);
  assert_dump(test, "<2></2>", ret2);
  assert_dump(test, "<2></2>", r_a);
  assert_dump(test, "<3></3>", r_b);
  assert_dump(test, "<3></3>", f);

  r_a.destroy();
  assert_dump(test, "<2></2>", ret2);

  var r_c = create("c", f);
  var r_d = create("d", f);
  var r_e = create("e", f);
  assert_dump(test, "<c><3></3></c>", r_c);
  assert_dump(test, "<d><c><3></3></c></d>", r_d);
  assert_dump(test, "<e><d><c><3></3></c></d></e>", r_e);
  assert_dump(test, "<1></1>", ret1);
  assert_dump(test, "<3></3>", r_b);

  r_d.destroy();
  assert_dump(test, "<3></3>", r_b);
  assert_dump(test, "<c><3></3></c>", r_c);
  assert_dump(test, "<e><c><3></3></c></e>", r_e);
  assert_dump(test, "<1></1>", ret1);

  assert_contained(r_e,
                   {range: r_e,
                    children: [{range: r_c,
                                children: [{range: r_b, children: []}]}]});

  test.equal(r_b.firstNode(), f.firstChild);
  test.equal(r_b.lastNode(), f.lastChild);
  test.equal(r_c.firstNode(), f.firstChild);
  test.equal(r_c.lastNode(), f.lastChild);
  test.equal(r_e.firstNode(), f.firstChild);
  test.equal(r_e.lastNode(), f.lastChild);

  r_b.destroy();
  assert_dump(test, "<c><3></3></c>", r_c);
  assert_dump(test, "<e><c><3></3></c></e>", r_e);

  r_e.destroy();
  assert_dump(test, "<c><3></3></c>", r_c);

});

Tinytest.add("liverange - empty replace", function (test) {
  var f,r;

  f = frag("

");
  r = create("z", f);
  test.throws(function() {
    r.replaceContents(frag(""));
  });

  f = frag("

");
  r = create("z", f.childNodes[1]);
  assert_dump(test, "<1></1><z><2></2></z><3></3>", f);
  test.throws(function() {
    r.replaceContents(frag(""));
  });
});

Tinytest.add("liverange - multiple nodes", function (test) {
  var f = frag("

");
  assert_dump(test, "<1></1><2></2><3></3><4></4><5></5>", f);

  var r_a = create("a", f.childNodes[2], f.childNodes[3]);
  assert_dump(test, "<1></1><2></2><3></3><4></4><5></5>", f);
  assert_dump(test, "<3></3><4></4>", r_a);

  var r_b = create("b", f.childNodes[3], f.childNodes[3]);
  assert_dump(test, "<1></1><2></2><3></3><4></4><5></5>", f);
  assert_dump(test, "<3></3><4></4>", r_a);
  assert_dump(test, "<4></4>", r_b);

  var r_c = create("c", f.childNodes[2], f.childNodes[3]);
  assert_dump(test, "<1></1><2></2><c><3></3><4></4></c><5></5>", f);
  assert_dump(test, "<3></3><4></4>", r_a);
  assert_dump(test, "<4></4>", r_b);
  assert_dump(test, "<c><3></3><4></4></c>", r_c);

  var r_d = create("d", f.childNodes[3], f.childNodes[3]);
  assert_dump(test, "<1></1><2></2><c><3></3><d><4></4></d></c><5></5>", f);
  assert_dump(test, "<3></3><d><4></4></d>", r_a);
  assert_dump(test, "<4></4>", r_b);
  assert_dump(test, "<c><3></3><d><4></4></d></c>", r_c);
  assert_dump(test, "<d><4></4></d>", r_d);

  var r_e = create("e", f.childNodes[2], f.childNodes[2]);
  assert_dump(test, "<1></1><2></2><c><e><3></3></e><d><4></4></d></c><5></5>", f);
  assert_dump(test, "<e><3></3></e><d><4></4></d>", r_a);
  assert_dump(test, "<4></4>", r_b);
  assert_dump(test, "<c><e><3></3></e><d><4></4></d></c>", r_c);
  assert_dump(test, "<d><4></4></d>", r_d);
  assert_dump(test, "<e><3></3></e>", r_e);

  var r_f = create("f", f.childNodes[2], f.childNodes[3]);
  assert_dump(test, "<1></1><2></2><f><c><e><3></3></e><d><4></4></d></c></f><5></5>", f);
  assert_dump(test, "<e><3></3></e><d><4></4></d>", r_a);
  assert_dump(test, "<4></4>", r_b);
  assert_dump(test, "<c><e><3></3></e><d><4></4></d></c>", r_c);
  assert_dump(test, "<d><4></4></d>", r_d);
  assert_dump(test, "<e><3></3></e>", r_e);
  assert_dump(test, "<f><c><e><3></3></e><d><4></4></d></c></f>", r_f);

  assert_contained(r_f, {range: r_f, children: [{range: r_c, children: [{range: r_a, children: [{range: r_e, children: []},{range: r_d, children: [{range: r_b, children: []}]}]}]}]});

  var r_g = create("g", f.childNodes[0], f.childNodes[3]);
  var r_h = create("h", f.childNodes[0], f.childNodes[3]);
  var r_i = create("i", f.childNodes[1], f.childNodes[3]);
  assert_dump(test, "<h><g><1></1><2></2><f><c><e><3></3></e><d><4></4></d></c></f></g></h><5></5>", f);
  assert_dump(test, "<e><3></3></e><d><4></4></d>", r_a);
  assert_dump(test, "<4></4>", r_b);
  assert_dump(test, "<c><e><3></3></e><d><4></4></d></c>", r_c);
  assert_dump(test, "<d><4></4></d>", r_d);
  assert_dump(test, "<e><3></3></e>", r_e);
  assert_dump(test, "<f><c><e><3></3></e><d><4></4></d></c></f>", r_f);
  assert_dump(test, "<g><1></1><2></2><f><c><e><3></3></e><d><4></4></d></c></f></g>", r_g);
  assert_dump(test, "<h><g><1></1><2></2><f><c><e><3></3></e><d><4></4></d></c></f></g></h>", r_h);
  assert_dump(test, "<2></2><f><c><e><3></3></e><d><4></4></d></c></f>", r_i);

  var f2 = frag("

");
  f2.childNodes[1].appendChild(f);
  assert_dump(test, "", f);
  assert_dump(test, "<6></6><7><h><g><1></1><2></2><f><c><e><3></3></e><d><4></4></d></c></f></g></h><5></5></7><8></8>", f2);
  assert_dump(test, "<e><3></3></e><d><4></4></d>", r_a);
  assert_dump(test, "<4></4>", r_b);
  assert_dump(test, "<c><e><3></3></e><d><4></4></d></c>", r_c);
  assert_dump(test, "<d><4></4></d>", r_d);
  assert_dump(test, "<e><3></3></e>", r_e);
  assert_dump(test, "<f><c><e><3></3></e><d><4></4></d></c></f>", r_f);
  assert_dump(test, "<g><1></1><2></2><f><c><e><3></3></e><d><4></4></d></c></f></g>", r_g);
  assert_dump(test, "<h><g><1></1><2></2><f><c><e><3></3></e><d><4></4></d></c></f></g></h>", r_h);
  assert_dump(test, "<2></2><f><c><e><3></3></e><d><4></4></d></c></f>", r_i);

  var r_j = create("j", f2.childNodes[1], f2.childNodes[2]);
  var r_k = create("k", f2.childNodes[0], f2.childNodes[2]);
  var r_l = create("l", f2.childNodes[0], f2.childNodes[2]);
  assert_dump(test, "<l><k><6></6><j><7><h><g><1></1><2></2><f><c><e><3></3></e><d><4></4></d></c></f></g></h><5></5></7><8></8></j></k></l>", f2);

  var f3 = frag("

");
  var r_m = create("m", f3.childNodes[0], f3.childNodes[2]);
  var r_n = create("n", f3.childNodes[0], f3.childNodes[0]);
  var r_o = create("o", f3.childNodes[0], f3.childNodes[0]);
  assert_dump(test, "<m><o><n><9></9></n></o><10></10><11></11></m>", f3);

  var ret1 = r_i.replaceContents(f3, true);
  assert_dump(test, "", f3);
  assert_dump(test, "<2></2><f><c><e><3></3></e><d><4></4></d></c></f>", ret1);
  assert_dump(test, "<l><k><6></6><j><7><h><g><1></1><m><o><n><9></9></n></o><10></10><11></11></m></g></h><5></5></7><8></8></j></k></l>", f2);
  assert_dump(test, "<e><3></3></e><d><4></4></d>", r_a);
  assert_dump(test, "<4></4>", r_b);
  assert_dump(test, "<c><e><3></3></e><d><4></4></d></c>", r_c);
  assert_dump(test, "<d><4></4></d>", r_d);
  assert_dump(test, "<e><3></3></e>", r_e);
  assert_dump(test, "<f><c><e><3></3></e><d><4></4></d></c></f>", r_f);
  assert_dump(test, "<g><1></1><m><o><n><9></9></n></o><10></10><11></11></m></g>", r_g);
  assert_dump(test, "<h><g><1></1><m><o><n><9></9></n></o><10></10><11></11></m></g></h>", r_h);
  assert_dump(test, "<m><o><n><9></9></n></o><10></10><11></11></m>",r_i);
  assert_dump(test, "<j><7><h><g><1></1><m><o><n><9></9></n></o><10></10><11></11></m></g></h><5></5></7><8></8></j>", r_j);
  assert_dump(test, "<k><6></6><j><7><h><g><1></1><m><o><n><9></9></n></o><10></10><11></11></m></g></h><5></5></7><8></8></j></k>", r_k);
  assert_dump(test, "<l><k><6></6><j><7><h><g><1></1><m><o><n><9></9></n></o><10></10><11></11></m></g></h><5></5></7><8></8></j></k></l>", r_l);

  r_h.destroy();
  assert_dump(test, "<l><k><6></6><j><7><g><1></1><m><o><n><9></9></n></o><10></10><11></11></m></g><5></5></7><8></8></j></k></l>", f2);
  r_m.destroy();
  assert_dump(test, "<l><k><6></6><j><7><g><1></1><o><n><9></9></n></o><10></10><11></11></g><5></5></7><8></8></j></k></l>", f2);
  r_n.destroy();
  assert_dump(test, "<l><k><6></6><j><7><g><1></1><o><9></9></o><10></10><11></11></g><5></5></7><8></8></j></k></l>", f2);
  r_j.destroy();
  assert_dump(test, "<l><k><6></6><7><g><1></1><o><9></9></o><10></10><11></11></g><5></5></7><8></8></k></l>", f2);
  r_o.destroy();
  assert_dump(test, "<l><k><6></6><7><g><1></1><9></9><10></10><11></11></g><5></5></7><8></8></k></l>", f2);
  r_g.destroy();
  assert_dump(test, "<l><k><6></6><7><1></1><9></9><10></10><11></11><5></5></7><8></8></k></l>", f2);
  r_l.destroy();
  assert_dump(test, "<k><6></6><7><1></1><9></9><10></10><11></11><5></5></7><8></8></k>", f2);
  r_i.destroy();
  assert_dump(test, "<k><6></6><7><1></1><9></9><10></10><11></11><5></5></7><8></8></k>", f2);
  r_k.destroy();
  assert_dump(test, "<6></6><7><1></1><9></9><10></10><11></11><5></5></7><8></8>", f2);
});

Tinytest.add("liverange - deep visit", function (test) {

  var f = frag("

");

  var dive = function (f, count) {
    for (var i = 0; i < count; i ++)
      f = f.firstChild;
    return f;
  };

  var r_a = create("a", dive(f, 5), dive(f, 5));
  var r_b = create("b", dive(f, 3), dive(f, 3));
  var r_c = create("c", dive(f, 2), dive(f, 2));
  var r_d = create("d", f);

  assert_dump(test, "<d><1><c><2><3><4><5></5></4></3></2></c></1></d>",
              f);

  assert_contained(r_d,
                   {range: r_d, children: [{range: r_c, children: [{range: r_b, children: [{range: r_a, children: []}]}]}]});
});

Tinytest.add("liverange - create inner", function (test) {
  // Basics

  var f = frag("

");
  assert_dump(test, "<1></1><2></2><3></3><4></4><5></5>", f);

  var r_a = create("a", f.childNodes[2], f.childNodes[4], true);
  assert_dump(test, "<1></1><2></2><3></3><4></4><5></5>", f);

  var r_b = create("b", f.childNodes[2], f.childNodes[4], true);
  assert_dump(test, "<1></1><2></2><3></3><4></4><5></5>", f);

  var r_c = create("c", f.childNodes[2], f.childNodes[4]);
  assert_dump(test, "<1></1><2></2><c><3></3><4></4><5></5></c>", f);

  // [{[a] [b]}]

  var r_d = create("d", f.childNodes[0], f.childNodes[0]);
  var r_e = create("e", f.childNodes[1], f.childNodes[1]);
  var r_f = create("f", f.childNodes[0], f.childNodes[1]);
  assert_dump(test, "<f><d><1></1></d><e><2></2></e></f><c><3></3><4></4><5></5></c>", f);

  var r_g = create("g", f.childNodes[0], f.childNodes[1], true);
  assert_dump(test, "<f><g><d><1></1></d><e><2></2></e></g></f><c><3></3><4></4><5></5></c>", f);

  var r_h = create("h", f.childNodes[0], f.childNodes[1]);
  assert_dump(test, "<h><f><g><d><1></1></d><e><2></2></e></g></f></h><c><3></3><4></4><5></5></c>", f);

  var r_i = create("i", f.childNodes[0], f.childNodes[1], true);
  assert_dump(test, "<h><f><g><d><1></1></d><e><2></2></e></g></f></h><c><3></3><4></4><5></5></c>", f);

  var r_j = create("j", f.childNodes[0], f.childNodes[0], true);
  assert_dump(test, "<h><f><g><d><j><1></1></j></d><e><2></2></e></g></f></h><c><3></3><4></4><5></5></c>", f);

  var r_k = create("k", f.childNodes[0], f.childNodes[0]);
  assert_dump(test, "<h><f><g><k><d><j><1></1></j></d></k><e><2></2></e></g></f></h><c><3></3><4></4><5></5></c>", f);

  var r_l = create("l", f.childNodes[0], f.childNodes[1], true);
  assert_dump(test, "<h><f><g><l><k><d><j><1></1></j></d></k><e><2></2></e></l></g></f></h><c><3></3><4></4><5></5></c>", f);
  assert_dump(test, "<c><3></3><4></4><5></5></c>", r_c);
  assert_dump(test, "<3></3><4></4><5></5>", r_b);
  assert_dump(test, "<3></3><4></4><5></5>", r_a);
  assert_dump(test, "<d><j><1></1></j></d>", r_d);
  assert_dump(test, "<e><2></2></e>", r_e);
  assert_dump(test, "<f><g><l><k><d><j><1></1></j></d></k><e><2></2></e></l></g></f>", r_f);
  assert_dump(test, "<g><l><k><d><j><1></1></j></d></k><e><2></2></e></l></g>", r_g);
  assert_dump(test, "<h><f><g><l><k><d><j><1></1></j></d></k><e><2></2></e></l></g></f></h>", r_h);
  assert_dump(test, "<l><k><d><j><1></1></j></d></k><e><2></2></e></l>", r_i);
  assert_dump(test, "<j><1></1></j>", r_j);
  assert_dump(test, "<k><d><j><1></1></j></d></k>", r_k);
  assert_dump(test, "<l><k><d><j><1></1></j></d></k><e><2></2></e></l>", r_l);

  // [{a b [c]}]
  f = frag("

");
  r_a = create("a", f.childNodes[2], f.childNodes[2]);
  r_b = create("b", f.childNodes[0], f.childNodes[2]);
  r_c = create("c", f.childNodes[0], f.childNodes[2], true);
  assert_dump(test, "<c><1></1><2></2><3></3></c>", f);

  // [{[a] b c}]

  f = frag("

");
  r_a = create("a", f.childNodes[0], f.childNodes[0]);
  r_b = create("b", f.childNodes[0], f.childNodes[2]);
  r_c = create("c", f.childNodes[0], f.childNodes[2], true);
  assert_dump(test, "<c><1></1><2></2><3></3></c>", f);

  // [{[a b] c}]

  f = frag("

");
  r_a = create("a", f.childNodes[0], f.childNodes[1]);
  r_b = create("b", f.childNodes[0], f.childNodes[2]);
  r_c = create("c", f.childNodes[0], f.childNodes[2], true);
  assert_dump(test, "<c><1></1><2></2><3></3></c>", f);

  // Cases where start and end have no common ranges, and so the
  // balance counter will have to run

  f = frag("

");
  r_a = create("a", f.childNodes[0], f.childNodes[0]);
  r_b = create("b", f.childNodes[0], f.childNodes[2]);
  assert_dump(test, "<1></1><2></2><3></3>", f);

  f = frag("

");
  r_a = create("a", f.childNodes[0], f.childNodes[2]);
  r_b = create("b", f.childNodes[0], f.childNodes[0]);
  assert_dump(test, "<1></1><2></2><3></3>", f);

  f = frag("

");
  r_a = create("a", f.childNodes[2], f.childNodes[2]);
  r_b = create("b", f.childNodes[0], f.childNodes[2]);
  assert_dump(test, "<1></1><2></2><3></3>", f);

  f = frag("

");
  r_a = create("a", f.childNodes[0], f.childNodes[2]);
  r_b = create("b", f.childNodes[2], f.childNodes[2]);
  assert_dump(test, "<1></1><2></2><3></3>", f);

  f = frag("

");
  r_a = create("a", f.childNodes[0], f.childNodes[0]);
  r_b = create("b", f.childNodes[0], f.childNodes[0]);
  r_c = create("c", f.childNodes[2], f.childNodes[2]);
  r_d = create("d", f.childNodes[2], f.childNodes[2]);
  r_e = create("e", f.childNodes[0], f.childNodes[2]);
  assert_dump(test, "<e><1></1><2></2><d><c><3></3></c></d></e>", f);

  f = frag("

");
  r_a = create("a", f.childNodes[0], f.childNodes[0]);
  r_b = create("b", f.childNodes[0], f.childNodes[0]);
  r_c = create("c", f.childNodes[2], f.childNodes[2]);
  r_e = create("e", f.childNodes[0], f.childNodes[2]);
  assert_dump(test, "<e><1></1><2></2><c><3></3></c></e>", f);

  try_all_permutations(
    function () {
      f = frag("

");
    },
    [
      function () { create("a", f.childNodes[1], f.childNodes[2]); },
      function () { create("b", f.childNodes[2], f.childNodes[2]); },
      function () { create("c", f.childNodes[0], f.childNodes[2]); }
    ],
    function () {
      assert_dump(test, "<c><1></1><2></2><3></3></c>", f);
    }
  );

  try_all_permutations(
    function () {
      f = frag("

");
    },
    [
      function () { create("a", f.childNodes[0], f.childNodes[0]); },
      function () { create("b", f.childNodes[0], f.childNodes[1]); },
      function () { create("c", f.childNodes[0], f.childNodes[2]); }
    ],
    function () {
      assert_dump(test, "<c><1></1><2></2><3></3></c>", f);
    }
  );
});

var makeTestPattern = function(codedStr) {
  codedStr = codedStr.replace(/\*/g, '[]');

  var self = {};
  self.tag = '_foo';
  self.ranges = {};

  // set up self.ranges
  var curNode = document.createDocumentFragment();
  var starts = [];
  for(var i=0; i<codedStr.length; i++) {
    var c = codedStr.charAt(i);
    if (/[A-Z]/.test(c)) {
      // open range
      starts.push([curNode, curNode.childNodes.length]);
    } else if (/[a-z]/.test(c)) {
      // close range
      var start = starts.pop();
      var range =
            new LiveRange(
              self.tag, start[0].childNodes[start[1]],
              start[0].lastChild);
      range.letter = c.toUpperCase();
      self.ranges[range.letter] = range;
    } else if (c === '[') {
      curNode.appendChild(document.createElement("DIV"));
      curNode = curNode.lastChild;
    } else if (c === ']') {
      // close node
      curNode = curNode.parentNode;
    }
  }

  self.frag = curNode;

  self.path = function(/*args*/) {
    var node = self.frag;
    _.each(arguments, function(i) {
      node = node.childNodes[i];
    });
    return node;
  };

  self.findRange = function(node) {
    return LiveRange.findRange(self.tag, node);
  };

  self.currentString = function() {
    var buf = [];
    var tempRange = new LiveRange(self.tag, self.frag);
    tempRange.visit(function(isStart, range) {
      buf.push(isStart ?
               range.letter.toUpperCase() :
               range.letter.toLowerCase());
    }, function(isStart, node) {
      buf.push(isStart ? '[' : ']');
    });
    tempRange.destroy();

    return buf.join('').replace(/\[\]/g, '*');
  };

  return self;
};

Tinytest.add("liverange - findParent", function(test) {
  var str = "I*[[AB[H***FDE*ed*fG*gh]*baC*c*]]i*";
  var pat = makeTestPattern(str);
  test.equal(pat.currentString(), str);

  var ranges = pat.ranges;

  test.equal(ranges.E.findParent().letter, 'D');
  test.equal(ranges.D.findParent().letter, 'F');
  test.equal(ranges.F.findParent().letter, 'H');
  test.equal(ranges.H.findParent().letter, 'B');
  test.equal(ranges.B.findParent().letter, 'A');
  test.equal(ranges.A.findParent().letter, 'I');
  test.equal(ranges.I.findParent(), null);

  test.equal(ranges.E.findParent(true).letter, 'D');
  test.equal(ranges.D.findParent(true).letter, 'F');
  test.equal(ranges.F.findParent(true).letter, 'H');
  test.equal(ranges.H.findParent(true), null);
  test.equal(ranges.B.findParent(true).letter, 'A');
  test.equal(ranges.A.findParent(true), null);
  test.equal(ranges.I.findParent(true), null);

  test.equal(pat.findRange(pat.path(0)).letter, 'I');
  test.equal(pat.findRange(pat.path(1)).letter, 'I');
  test.equal(pat.findRange(pat.path(2)), null);

  test.equal(pat.findRange(pat.path(1, 0)).letter, 'I');
  test.equal(pat.findRange(pat.path(1, 0, 0)).letter, 'B');
  test.equal(pat.findRange(pat.path(1, 0, 1)).letter, 'B');
  test.equal(pat.findRange(pat.path(1, 0, 2)).letter, 'C');
  test.equal(pat.findRange(pat.path(1, 0, 3)).letter, 'I');

  test.equal(pat.findRange(pat.path(1, 0, 0, 0)).letter, 'H');
  test.equal(pat.findRange(pat.path(1, 0, 0, 1)).letter, 'H');
  test.equal(pat.findRange(pat.path(1, 0, 0, 2)).letter, 'H');
  test.equal(pat.findRange(pat.path(1, 0, 0, 3)).letter, 'E');
  test.equal(pat.findRange(pat.path(1, 0, 0, 4)).letter, 'F');
  test.equal(pat.findRange(pat.path(1, 0, 0, 5)).letter, 'G');

});

Tinytest.add("liverange - destroy", function(test) {
  var str = "I*[[AB[H***FDE*ed*fG*gh]*baC*c*]]J*ji*";
  var pat = makeTestPattern(str);

  pat.ranges.D.destroy();
  test.equal(pat.currentString(), str.replace(/[Dd]/g, ''));
  pat.ranges.B.destroy();
  test.equal(pat.currentString(), str.replace(/[DdBb]/g, ''));
  pat.ranges.A.destroy();
  test.equal(pat.currentString(), str.replace(/[DdBbAa]/g, ''));

  // recursive destroy
  pat.ranges.F.destroy(true);
  test.equal(pat.currentString(),
             "I*[[[H*****G*gh]*C*c*]]J*ji*");
  pat.ranges.I.destroy(true);
  test.equal(pat.currentString(),
             "*[[[******]***]]**");

  var childrenHaveNoTags = function(node) {
    for(var n = node.firstChild; n; n = n.nextSibling) {
      test.isFalse(node[pat.tag]);
      if (n.firstChild)
        childrenHaveNoTags(n); // recurse
    }
  };

  childrenHaveNoTags(pat.frag);

  // test recursive on single node
  var frag = document.createDocumentFragment();
  var txt = document.createComment("pudding");
  frag.appendChild(txt);
  var rng5 = new LiveRange('_pudding', txt);
  var rng4 = new LiveRange('_pudding', txt);
  var rng3 = new LiveRange('_pudding', txt);
  var rng2 = new LiveRange('_pudding', txt);
  var rng1 = new LiveRange('_pudding', txt);
  rng1.num = 1;
  rng2.num = 2;
  rng3.num = 3;
  rng4.num = 4;
  rng5.num = 5;
  // kill an inner range
  rng4.destroy(true);
  // check that outer ranges are still there
  var buf = [];
  rng1.visit(function(isStart, r) {
    buf.push([isStart, r.num]);
  });
  test.equal(buf, [[true, 2], [true, 3], [false, 3], [false, 2]]);
});


# Meteor packages used by this project, one per line.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.

insecure
preserve-inputs
bootstrap
random


Package.describe({
  summary: "Reactive dictionary",
  internal: true
});

Package.on_use(function (api, where) {
  where = where || ['client', 'server'];

  api.use(['underscore', 'deps'], where);
  api.add_files('reactive-dict.js', where);
});

Package.on_test(function (api) {
  api.use('tinytest');
});


var path = require('path');
var files = require(path.join(__dirname, 'files.js'));
var fs = require('fs');
var _ = require('underscore');
var crypto = require('crypto');

var sha1 = function (contents) {
  var hash = crypto.createHash('sha1');
  hash.update(contents);
  return hash.digest('hex');
};

// Builder encapsulates much of the file-handling logic need to create
// "bundles" (directory trees such as site archives, programs, or
// packages.) It can create a temporary directory in which to build
// the bundle, moving the bundle atomically into place when and if the
// build successfully completes; sanitize and generate unique
// filenames; and track dependencies (files that should be watched for
// changes when developing interactively.)
//
// Options:
//  - outputPath: Required. Path to the directory that will hold the
//    bundle when building is complete. It should not exist. Its
//    parents will be created if necessary.
//  - symlink: if true, symlink rather than copy files/directories
//    where possible. This is faster and takes up less disk space but
//    produces a bundle that can't run elsewhere, and also that will
//    reflect changes to the input files in realtime (we don't care
//    about that, at least in development mode, since we reload on
//    change anyway..)
var Builder = function (options) {
  var self = this;
  options = options || {};

  self.outputPath = options.outputPath;

  // Paths already written to. Map from canonicalized relPath (no
  // trailing slash) to true for a file, or false for a directory.
  self.usedAsFile = { '': false, '.': false };

  self.shouldSymlink = !! options.symlink;

  // foo/bar => foo/.build1234.bar
  // Should we include a random number? The advantage is that multiple
  // builds can run in parallel. The disadvantage is that stale build
  // files hang around forever. For now, go with the former.
  var nonce = Math.floor(Math.random() * 999999);
  self.buildPath = path.join(path.dirname(self.outputPath),
                             '.build' + nonce + "." +
                             path.basename(self.outputPath));
  files.rm_recursive(self.buildPath);
  files.mkdir_p(self.buildPath, 0755);

  self.dependencyInfo = { directories: {}, files: {} };

  // XXX cleaner error handling. don't make the humans read an
  // exception (and, make suitable for use in automated systems)
};

_.extend(Builder.prototype, {
  // Like mkdir_p, but records in self.usedAsFile that we have created
  // the directories, and takes a path relative to the bundle
  // root. Throws an exception on failure.
  _ensureDirectory: function (relPath) {
    var self = this;

    var parts = path.normalize(relPath).split(path.sep);
    if (parts.length > 1 && parts[parts.length - 1] === '')
      parts.pop(); // remove trailing slash

    var partsSoFar = [];
    _.each(parts, function (part) {
      partsSoFar.push(part);
      var partial = partsSoFar.join(path.sep);
      if (! (partial in self.usedAsFile)) {
        // It's new -- create it
        fs.mkdirSync(path.join(self.buildPath, partial), 0755);
        self.usedAsFile[partial] = false;
      } else if (self.usedAsFile[partial]) {
        // Already exists and is a file. Oops.
        throw new Error("tried to make " + relPath + " a directory but " +
                        partial + " is already a file");
      } else {
        // Already exists and is a directory
      }
    });
  },

  // isDirectory defaults to false
  _sanitize: function (relPath, isDirectory) {
    var self = this;

    var parts = relPath.split(path.sep);
    var partsOut = [];
    for (var i = 0; i < parts.length; i++) {
      var part = parts[i];
      var shouldBeFile = (i === parts.length - 1) && ! isDirectory;

      // Basic sanitization
      if (part.match(/^\.*$/))
        throw new Error("Path contains forbidden segment '" + part + "'");
      part = part.replace(/[^a-zA-Z0-9._-]/g, '');

      // If at last component, pull extension (if any) off of part
      var ext = '';
      if (shouldBeFile) {
        var split = part.split('.');
        if (split.length > 1)
          ext = "." + split.pop();
        part = split.join('.');
      }

      // Make sure it's sufficiently unique
      var suffix = '';
      while (true) {
        var candidate = path.join(partsOut.join(path.sep), part + suffix + ext);
        if (! (candidate in self.usedAsFile) ||
            (shouldBeFile === self.usedAsFile[candidate]))
          // No conflict -- either not used, or it's two paths that
          // share a common ancestor directory (as opposed to one path
          // thinking that a/b should be a file, and another thinking
          // that it should be a directory)
          break;

        suffix++; // first increment will do '' -> 1
      }

      partsOut.push(part + suffix + ext);
    }

    return partsOut.join(path.sep);
  },

  // Write either a buffer or the contents of a file to `relPath` (a
  // path to a file relative to the bundle root), creating it (and any
  // enclosing directories) if it doesn't exist yet. Exactly one of
  // `data` and or `file` must be passed.
  //
  // Options:
  // - data: a Buffer to write to relPath.
  // - file: a filename to write to relPath, as a string.
  // - sanitize: if true, then all components of the path are stripped
  //   of any potentially troubling characters, an exception is thrown
  //   if any path segments consist entirely of dots (eg, '..'), and
  //   if there is a file in the bundle with the same relPath, then
  //   the path is changed by adding a numeric suffix.
  // - append: if true, append to the file if it exists rather than
  //   throwing an exception.
  //
  // Returns the final canonicalize relPath that was written to.
  //
  // If data was a filename then a dependency will be added on that
  // file.
  write: function (relPath, options) {
    var self = this;
    options = options || {};

    // Ensure no trailing slash
    if (relPath.slice(-1) === path.sep)
      relPath = relPath.slice(0, -1);

    // In sanitize mode, ensure path does not contain segments like
    // '..', does not contain forbidden characters, and is unique.
    if (options.sanitize)
      relPath = self._sanitize(relPath);

    var data;
    if (options.data) {
      if (! (options.data instanceof Buffer))
        throw new Error("data must be a Buffer");
      if (options.file)
        throw new Error("May only pass one of data and file, not both");
      data = options.data;
    } else if (options.file) {
      var sourcePath = path.resolve(options.file);
      data = fs.readFileSync(sourcePath);
      self.dependencyInfo.files[sourcePath] = sha1(data);
    }

    self._ensureDirectory(path.dirname(relPath));
    var absPath = path.join(self.buildPath, relPath);
    if (options.append)
      fs.appendFileSync(absPath, data);
    else
      fs.writeFileSync(absPath, data);
    self.usedAsFile[relPath] = true;

    return relPath;
  },

  // Serialize `data` as JSON and write it to `relPath` (a path to a
  // file relative to the bundle root), creating parent directories as
  // necessary. Throw an exception if the file already exists.
  writeJson: function (relPath, data) {
    var self = this;

    // Ensure no trailing slash
    if (relPath.slice(-1) === path.sep)
      relPath = relPath.slice(0, -1);

    fs.writeFileSync(path.join(self.buildPath, relPath),
                     new Buffer(JSON.stringify(data, null, 2), 'utf8'));

    self.usedAsFile[relPath] = true;
  },

  // Add relPath to the list of "already taken" paths in the
  // bundle. This will cause write, when in sanitize mode, to never
  // pick this filename (and will prevent files that from being
  // written that would conflict with paths that we are expecting to
  // be directories.) Calling this twice on the same relPath will
  // given an exception.
  //
  // options:
  // - directory: set to true to reserve this relPath to be a
  //   directory rather than a file.
  reserve: function (relPath, options) {
    var self = this;
    options = options || {};

    // Ensure no trailing slash
    if (relPath.slice(-1) === path.sep)
      relPath = relPath.slice(0, -1);

    var parts = relPath.split(path.sep);
    var partsSoFar = [];
    for (var i = 0; i < parts.length; i ++) {
      var part = parts[i];
      partsSoFar.push(part);
      var soFar = partsSoFar.join(path.sep);
      if (self.usedAsFile[soFar])
        throw new Error("Path reservation conflict: " + relPath);

      var shouldBeDirectory = (i < parts.length - 1) || options.directory;
      if (shouldBeDirectory) {
        if (! soFar in self.usedAsFile) {
          fs.mkdirSync(path.join(self.buildPath, soFar), 0755);
          self.usedAsFile[soFar] = false;
        }
      } else {
        self.usedAsFile[soFar] = true;
      }
    }
  },

  // Generate and reserve a unique name for a file based on `relPath`,
  // and return it. If `relPath` is available (there is no file with
  // that name currently existing or reserved, it doesn't contain
  // forbidden characters, a prefix of it is not already in use as a
  // file rather than a directory) then the return value will be
  // `relPath`. Otherwise relPath will be modified to get the return
  // value, say by adding a numeric suffix to some path components
  // (preserving the file extension however) and deleting forbidden
  // characters. Throws an exception if relPath contains any segments
  // that are all dots (eg, '..').
  //
  // options:
  //
  // - directory: generate (and reserve) a name for a directory,
  //   rather than a file.
  generateFilename: function (relPath, options) {
    var self = this;
    options = options || {};

    relPath = self._sanitize(relPath, options.directory);
    self.reserve(relPath, { directory: options.directory });
    return relPath;
  },

  // Recursively copy a directory and all of its contents into the
  // bundle. But if the symlink option was passed to the Builder
  // constructor, then make a symlink instead, if possible.
  //
  // Adds dependencies both on the files that were copied, and on the
  // contents of the directory tree (respecting 'ignore'.) Disable
  // this with depend: false.
  //
  // Options:
  // - from: source path on local disk to copy from
  // - to: relative path to a directory in the bundle that will
  //   receive the files
  // - ignore: array of regexps of filenames (that is, basenames) to
  //   ignore (they may still be visible in the output bundle if
  //   symlinks are being used)
  // - depend: Should dependencies be added? Defaults to true.
  copyDirectory: function (options) {
    var self = this;
    options = options || {};

    var createDependencies =
      ('depend' in options) ? options.depend : true;

    var normOptionsTo = options.to;
    if (normOptionsTo.slice(-1) === path.sep)
      normOptionsTo = normOptionsTo.slice(0, -1);

    var absPathTo = path.join(self.buildPath, normOptionsTo);
    if (self.shouldSymlink && ! (normOptionsTo in self.usedAsFile)) {
      self._ensureDirectory(path.dirname(normOptionsTo));
      fs.symlinkSync(path.resolve(options.from), absPathTo);
      return;
    }

    var ignore = options.ignore || [];
    if (createDependencies) {
      self.dependencyInfo.directories[absPathTo] = {
        include: [/.?/],
        exclude: ignore
      };
    }

    var walk = function (absFrom, relTo) {
      self._ensureDirectory(relTo);

      _.each(fs.readdirSync(absFrom), function (item) {
        if (_.any(ignore, function (pattern) {
          return item.match(pattern);
        })) return; // skip excluded files

        var thisAbsFrom = path.resolve(absFrom, item);
        var thisRelTo = path.join(relTo, item);
        if (fs.statSync(thisAbsFrom).isDirectory()) {
          walk(thisAbsFrom, thisRelTo);
          return;
        }

        // XXX avoid reading whole file into memory
        var data = fs.readFileSync(thisAbsFrom);

        if (createDependencies)
          self.dependencyInfo.files[thisAbsFrom] = sha1(data);

        fs.writeFileSync(path.resolve(self.buildPath, thisRelTo), data);
        self.usedAsFile[thisRelTo] = true;
      });
    };

    walk(options.from, normOptionsTo);
  },

  // Returns a new Builder-compatible object that works just like a
  // Builder, but interprets all paths relative to 'relPath', a path
  // relative to the bundle root.
  //
  // The sub-builder returned does not have all Builder methods (for
  // example, complete() wouldn't make sense) and you should not rely
  // on it beig instanceof Builder.
  enter: function (relPath) {
    var self = this;
    var methods = ["write", "writeJson", "reserve", "copyDirectory",
                   "enter"];
    var ret = {};

    _.each(methods, function (method) {
      ret[method] = function (/* arguments */) {
        args = _.toArray(arguments);

        if (method !== "copyDirectory") {
          // Normal method (relPath as first argument)
          args = _.clone(args);
          args[0] = path.join(relPath, args[0]);
        } else {
          // with copyDirectory the path we have to fix up is inside
          // an options hash
          args[0] = _.clone(args[0]);
          args[0].to = path.join(relPath, args[0].to);
        }

        return self[method].apply(self, args);
      };
    });

    return ret;
  },

  // Move the completed bundle into its final location (outputPath)
  complete: function () {
    var self = this;
    files.rm_recursive(self.outputPath);
    fs.renameSync(self.buildPath, self.outputPath);
  },

  // Delete the partially-completed bundle. Do not disturb outputPath.
  abort: function () {
    var self = this;
    files.rm_recursive(self.buildPath);
  },

  // Return all dependency info that has accumulated, in the format
  // expected by watch.Watcher.
  getDependencyInfo: function () {
    var self = this;
    return self.dependencyInfo;
  }
});

// static convenience method
Builder.sha1 = sha1;

module.exports = Builder;


Meteor.__COFFEESCRIPT_PRESENT = true

@__COFFEESCRIPT_TEST_GLOBAL = 123

Tinytest.add "coffeescript - compile", (test) -> test.isTrue true