// Generated by CoffeeScript 1.9.3

/*
This class build whole codebase for path

Короче, здесь у нас модуль, который получает на вход 
путь и возвращает полный комплект из путей (для разрешения require)
и скомпилированное содерживое файлов
 */

(function() {
  var Gatherer, LRU, Resolver, _, async, detective, fs, path, util,
    bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
    slice = [].slice;

  fs = require('fs');

  path = require('path');

  _ = require('lodash');

  async = require('async');

  util = require('util');

  detective = require('detective');

  LRU = require('lru-cache');

  Resolver = require('async-resolve');

  module.exports = Gatherer = (function() {
    var MAX_AGE, QUEUE_CONCURRENCY, ROOT_PARENT;

    MAX_AGE = 1000 * 60 * 60 * 10;

    ROOT_PARENT = '.';

    QUEUE_CONCURRENCY = 20;

    function Gatherer(_digest_calculator_, _file_processor_, _file_loader_, _options_) {
      var ref;
      this._digest_calculator_ = _digest_calculator_;
      this._file_processor_ = _file_processor_;
      this._file_loader_ = _file_loader_;
      this._options_ = _options_ != null ? _options_ : {};
      this._dependenciesTreeSaver = bind(this._dependenciesTreeSaver, this);
      this._findRequiresAndAddToQueue = bind(this._findRequiresAndAddToQueue, this);
      this._queueFn = bind(this._queueFn, this);
      this._buildModulePack = bind(this._buildModulePack, this);
      this._getChangedFiles = bind(this._getChangedFiles, this);
      this._cleanReBuildedModulePack = bind(this._cleanReBuildedModulePack, this);
      this._reBuildModulePack = bind(this._reBuildModulePack, this);
      this._extendOptionsRequireless = bind(this._extendOptionsRequireless, this);
      this._verifyAndBuildModulePack = bind(this._verifyAndBuildModulePack, this);
      this._getStepCache = bind(this._getStepCache, this);
      this.buildModulePack = bind(this.buildModulePack, this);
      this._pathfinder_ = new Resolver();
      (ref = this._pathfinder_).addExtensions.apply(ref, this._file_processor_.getSupportedFileExtentions());
      this._require_cache_ = LRU({
        max: 1000,
        maxAge: MAX_AGE
      });
      this._hard_cache_ = LRU({
        max: 100,
        maxAge: MAX_AGE
      });
    }


    /*
    This method reset all caches
     */

    Gatherer.prototype.resetCaches = function() {
      this._require_cache_.reset();
      this._hard_cache_.reset();
      return null;
    };


    /*
    This is 'lite' version of packer - it buld pack from function
    used in replacement as function type
     */

    Gatherer.prototype.buildFunctionPack = function(raw_code, main_cb) {
      var digest, digest_polindrome, result;
      result = {
        dependencies_tree: {},
        names_map: {},
        source_code: {}
      };
      digest = this._digest_calculator_.calculateDataDigest(raw_code);
      digest_polindrome = {};
      digest_polindrome[digest] = digest;
      result.dependencies_tree[ROOT_PARENT] = digest_polindrome;
      result.names_map = digest_polindrome;
      result.source_code[digest] = "\nmodule.exports = (" + (raw_code != null ? raw_code.toString() : void 0) + "\n)()\n";
      return main_cb(null, result);
    };


    /*
    This is Async version of packer
    
    TODO @meetya add some request blocker (if builder called in sequence)
     */

    Gatherer.prototype.buildModulePack = function(path_name, options, main_cb) {
      var done_cb, step_cache_name;
      if (options == null) {
        options = {};
      }
      step_cache_name = this._getStepCache(path_name, options);
      done_cb = (function(_this) {
        return function(err, data) {
          if (err != null) {
            return main_cb(err);
          }
          _this._hard_cache_.set(step_cache_name, data);
          return main_cb(null, _.cloneDeep(data));
        };
      })(this);
      if (!this._hard_cache_.has(step_cache_name)) {
        return this._buildModulePack(path_name, options, step_cache_name, done_cb);
      } else {
        return this._verifyAndBuildModulePack(options, step_cache_name, done_cb);
      }
    };


    /*
    This method calculate step cache name
     */

    Gatherer.prototype._getStepCache = function(path_name, options) {
      return this._digest_calculator_.calculateDataDigest(path_name + util.inspect(options));
    };


    /*
    This is verified rebuilder step
     */

    Gatherer.prototype._verifyAndBuildModulePack = function(options, step_cache_name, main_cb) {
      var probably_res;
      probably_res = this._hard_cache_.get(step_cache_name);
      return this._getChangedFiles(probably_res.names_map, (function(_this) {
        return function(err, changed_list) {
          var extended_options;
          if (!changed_list.length) {
            return main_cb(null, probably_res);
          } else {
            extended_options = _this._extendOptionsRequireless({
              options: options,
              changed: changed_list,
              all_files: _.keys(probably_res.names_map)
            });
            return _this._reBuildModulePack(changed_list, extended_options, step_cache_name, function(err, new_data) {
              var changed_data, i, len, member_key, member_value;
              if (err) {
                return main_cb(err);
              }
              for (i = 0, len = new_data.length; i < len; i++) {
                changed_data = new_data[i];
                for (member_key in changed_data) {
                  member_value = changed_data[member_key];
                  _.assign(probably_res[member_key], member_value);
                }
              }
              return main_cb(null, _this._cleanReBuildedModulePack(probably_res));
            });
          }
        };
      })(this));
    };


    /*
    This method extend options for rebuild module pack function to avoid re-calculate unchanged chidrens
     */

    Gatherer.prototype._extendOptionsRequireless = function(arg) {
      var all_files, changed, options;
      options = arg.options, changed = arg.changed, all_files = arg.all_files;
      options.unchanged = _.difference(all_files, changed);
      return options;
    };


    /*
    This is module pack rebuilder - in case something changed
     */

    Gatherer.prototype._reBuildModulePack = function(changed_list, options, step_cache_name, main_cb) {
      var map_fn;
      map_fn = (function(_this) {
        return function(item, acb) {
          return _this._file_loader_.isFileExists(item, function(is_exist) {
            var ref;
            if (is_exist) {
              return _this._buildModulePack(item, options, step_cache_name, function(err, data) {
                var ref;
                if (err != null) {
                  return acb(err);
                }
                if ((data != null ? (ref = data.dependencies_tree) != null ? ref[ROOT_PARENT] : void 0 : void 0) != null) {
                  delete data.dependencies_tree[ROOT_PARENT];
                }
                return acb(null, data);
              });
            } else {
              if ((typeof data !== "undefined" && data !== null ? (ref = data.dependencies_tree) != null ? ref[ROOT_PARENT] : void 0 : void 0) != null) {
                delete data.dependencies_tree[ROOT_PARENT];
              }
              return acb(null);
            }
          });
        };
      })(this);
      return async.map(changed_list, map_fn, main_cb);
    };


    /*
    This method recursive clean up orphan elements
     */

    Gatherer.prototype._cleanReBuildedModulePack = function(raw_data, prev_used_parts) {
      var key, used_parts, used_parts_dict;
      if (prev_used_parts == null) {
        prev_used_parts = [];
      }
      used_parts = _.chain(raw_data.dependencies_tree).values().map(function(elem) {
        return _.values(elem);
      }).flatten().uniq().push('.').value();
      used_parts_dict = _.reduce(used_parts, (function(acc, elem) {
        acc[elem] = true;
        return acc;
      }), {});
      for (key in raw_data.dependencies_tree) {
        if (!used_parts_dict[key]) {
          delete raw_data.dependencies_tree[key];
        }
      }
      if (!_.isEqual(prev_used_parts, used_parts)) {
        return this._cleanReBuildedModulePack(raw_data, used_parts);
      }
      for (key in raw_data.names_map) {
        if (!used_parts_dict[key]) {
          delete raw_data.names_map[key];
        }
      }
      for (key in raw_data.source_code) {
        if (!used_parts_dict[key]) {
          delete raw_data.source_code[key];
        }
      }
      return raw_data;
    };


    /*
    This is fast file tester - return changed files list
     */

    Gatherer.prototype._getChangedFiles = function(names_map, cb) {
      var some_fn, vec;
      vec = _.keys(names_map);
      some_fn = (function(_this) {
        return function(file, acb) {
          return _this._file_loader_.readCachedFileDigest(file, function(err, res) {
            if (err != null) {
              return acb(false);
            }
            return acb(res === names_map[file]);
          });
        };
      })(this);
      return async.reject(vec, some_fn, function(results) {
        return cb(null, results);
      });
    };


    /*
    internal method
     */

    Gatherer.prototype._buildModulePack = function(path_name, options, step_cache_name, main_cb) {
      var load_queue, pack_cache;
      pack_cache = {
        dependencies_tree: {},
        names_map: {},
        source_code: {},
        err: null,
        filters: [],
        requireless: [],
        unchanged: [],
        step_cache_name: step_cache_name
      };
      if (options.filters != null) {
        pack_cache.filters = this._forceFilterToArray(options.filters);
      }
      if (options.requireless != null) {
        pack_cache.requireless = this._forceFilterToArray(options.requireless);
      }
      if (options.unchanged != null) {
        pack_cache.unchanged = this._forceFilterToArray(options.unchanged);
      }
      pack_cache.queue_obj = load_queue = async.queue(this._queueFn, QUEUE_CONCURRENCY);
      load_queue.drain = (function(_this) {
        return function() {
          var res;
          if (!pack_cache.err) {
            res = {
              dependencies_tree: pack_cache.dependencies_tree,
              names_map: pack_cache.names_map,
              source_code: pack_cache.source_code
            };
            return main_cb(null, res);
          } else {
            return main_cb(pack_cache.err);
          }
        };
      })(this);
      return load_queue.push({
        path_name: path_name,
        parent: ROOT_PARENT,
        pack_cache: pack_cache
      }, function(err) {
        if (err != null) {
          return pack_cache.err = err;
        }
      });
    };


    /*
    Oh!
    Many things here, but its price of async code
     */

    Gatherer.prototype._queueFn = function(arg, queue_cb) {
      var pack_cache, parent, path_name;
      path_name = arg.path_name, parent = arg.parent, pack_cache = arg.pack_cache;
      return async.waterfall([
        (function(_this) {
          return function(waterfall_cb) {
            return _this._pathfinder_.resolveAbsolutePath(path_name, path.dirname(parent), function(err, data) {
              return waterfall_cb(err, data);
            });
          };
        })(this), (function(_this) {
          return function(real_file_name, waterfall_cb) {
            if (!_this._dependenciesTreeSaver({
              path_name: path_name,
              parent: parent,
              real_file_name: real_file_name,
              pack_cache: pack_cache
            })) {
              return queue_cb();
            }
            return _this._file_processor_.loadFile(real_file_name, function(err, content, may_have_reqire, file_props) {
              if (err != null) {
                return waterfall_cb(err);
              }
              return waterfall_cb(null, {
                digest: file_props.digest,
                content: content,
                may_have_reqire: may_have_reqire,
                path_name: path_name,
                real_file_name: real_file_name
              });
            });
          };
        })(this), (function(_this) {
          return function(arg1, waterfall_cb) {
            var content, digest, may_have_reqire, path_name, real_file_name;
            digest = arg1.digest, content = arg1.content, may_have_reqire = arg1.may_have_reqire, path_name = arg1.path_name, real_file_name = arg1.real_file_name;
            _this._fileDataSaver({
              digest: digest,
              content: content,
              real_file_name: real_file_name,
              pack_cache: pack_cache
            });
            return _this._findRequiresAndAddToQueue({
              digest: digest,
              may_have_reqire: may_have_reqire,
              content: content,
              real_file_name: real_file_name,
              path_name: path_name,
              pack_cache: pack_cache,
              waterfall_cb: waterfall_cb
            });
          };
        })(this)
      ], (function(_this) {
        return function(err) {
          return queue_cb(err);
        };
      })(this));
    };


    /*
    This is converter for ensure filter is Array
    @arg may be one value or Array
     */

    Gatherer.prototype._forceFilterToArray = function() {
      var filters_list, first_filter, other_filters;
      first_filter = arguments[0], other_filters = 2 <= arguments.length ? slice.call(arguments, 1) : [];
      return filters_list = !_.isArray(first_filter) ? [first_filter].concat(other_filters) : first_filter;
    };


    /*
    This method searching for requires, its just stub.
    later I re-write it to class
    to substitute detective with my own logic and acorn
     */

    Gatherer.prototype._findRequiresItself = function(data) {
      var error, result;
      result = [];
      try {
        result = detective(data);
      } catch (_error) {
        error = _error;
        return [error];
      }
      return [null, result];
    };


    /*
    Build composite digest for files
    
    its needed because some file may have different requires list by use differtent settings
     */

    Gatherer.prototype._getCompositeDigest = function(step_cache_name, digest) {
      return step_cache_name + "_" + digest;
    };


    /*
    This method find requires in files, if they need it and 
    add to queue new files for recurse working
     */

    Gatherer.prototype._findRequiresAndAddToQueue = function(arg) {
      var childrens, composite_digest, content, digest, eachFn, err, may_have_reqire, pack_cache, path_name, real_file_name, ref, res, waterfall_cb;
      digest = arg.digest, may_have_reqire = arg.may_have_reqire, content = arg.content, real_file_name = arg.real_file_name, path_name = arg.path_name, pack_cache = arg.pack_cache, waterfall_cb = arg.waterfall_cb;

      /*
      console.log 'path_name'
      console.log path_name
      console.log 'real_file_name'
      console.log real_file_name
      
      console.log 'pack_cache.unchanged'
      console.log pack_cache.unchanged
       */
      if (!(may_have_reqire || !this._isFilesMustBeProcessed(pack_cache.requireless, path_name))) {
        return waterfall_cb();
      }
      composite_digest = this._getCompositeDigest(pack_cache.step_cache_name, digest);
      if (!this._require_cache_.has(composite_digest)) {
        ref = this._findRequiresItself(content), err = ref[0], res = ref[1];
        if (err != null) {
          err.fileName = real_file_name;
          return waterfall_cb(err);
        } else {
          this._require_cache_.set(composite_digest, res);
          childrens = res;
        }
      } else {
        childrens = this._require_cache_.get(composite_digest);
      }
      eachFn = (function(_this) {
        return function(child, acb) {
          return _this._pathfinder_.resolveAbsolutePath(child, path.dirname(real_file_name), function(err, real_child_name) {
            if (err != null) {
              return acb(err);
            }
            if (_this._isFilesMustBeProcessed(pack_cache.unchanged, real_child_name)) {
              pack_cache.queue_obj.push({
                path_name: child,
                parent: real_file_name,
                pack_cache: pack_cache
              }, function(err) {
                if (err != null) {
                  return pack_cache.err = err;
                }
              });
            } else {
              _this._dependenciesTreeSaver({
                path_name: child,
                parent: real_file_name,
                real_file_name: real_child_name,
                pack_cache: pack_cache
              });
            }
            return acb();
          });
        };
      })(this);
      return async.each(childrens, eachFn, (function(_this) {
        return function(err) {
          if (err != null) {
            return waterfall_cb(err);
          }
          return waterfall_cb();
        };
      })(this));
    };


    /*
    This part-method handle save to tree
    if file exists or unnided - return false
     */

    Gatherer.prototype._dependenciesTreeSaver = function(arg) {
      var base, dep_tree_par, pack_cache, parent, path_name, real_file_name;
      path_name = arg.path_name, parent = arg.parent, real_file_name = arg.real_file_name, pack_cache = arg.pack_cache;
      dep_tree_par = (base = pack_cache.dependencies_tree)[parent] != null ? base[parent] : base[parent] = {};
      if (dep_tree_par[path_name] != null) {
        return false;
      }
      if (!this._isFilesMustBeProcessed(pack_cache.filters, path_name)) {
        dep_tree_par[path_name] = null;
        return false;
      }
      if (this._pathfinder_.isCoreModule(real_file_name)) {
        dep_tree_par[path_name] = null;
        return false;
      }
      dep_tree_par[path_name] = real_file_name;
      return true;
    };


    /*
    This method save data
    void return
     */

    Gatherer.prototype._fileDataSaver = function(arg) {
      var base, content, digest, pack_cache, real_file_name;
      digest = arg.digest, content = arg.content, real_file_name = arg.real_file_name, pack_cache = arg.pack_cache;
      pack_cache.names_map[real_file_name] = digest;
      if ((base = pack_cache.source_code)[real_file_name] == null) {
        base[real_file_name] = content;
      }
      return null;
    };


    /*
    This is proceed filter
    return 'true' if file MUST be processed, 
    ie it filename NOT in list - it will be processed
     */

    Gatherer.prototype._isFilesMustBeProcessed = function(filters_list, path_name) {
      return !_.any(filters_list, function(filter) {
        return filter === path_name;
      });
    };

    return Gatherer;

  })();

}).call(this);
