"use strict";

var Cache = require("./cache.js").Cache;
var tuple = require("immutable-tuple").tuple;
var Entry = require("./entry.js").Entry;
var getLocal = require("./local.js").get;

// Exported so that custom makeCacheKey functions can easily reuse the
// default implementation (with different arguments).
exports.defaultMakeCacheKey = tuple;

function normalizeOptions(options) {
  options = options || Object.create(null);

  if (typeof options.makeCacheKey !== "function") {
    options.makeCacheKey = tuple;
  }

  if (typeof options.max !== "number") {
    options.max = Math.pow(2, 16);
  }

  return options;
}

function wrap(fn, options) {
  options = normalizeOptions(options);

  // If this wrapped function is disposable, then its creator does not
  // care about its return value, and it should be removed from the cache
  // immediately when it no longer has any parents that depend on it.
  var disposable = !! options.disposable;

  var cache = new Cache({
    max: options.max,
    dispose: function (key, entry) {
      entry.dispose();
    }
  });

  function reportOrphan(entry) {
    if (disposable) {
      // Triggers the entry.dispose() call above.
      cache.delete(entry.key);
      return true;
    }
  }

  function optimistic() {
    if (disposable && ! getLocal().currentParentEntry) {
      // If there's no current parent computation, and this wrapped
      // function is disposable (meaning we don't care about entry.value,
      // just dependency tracking), then we can short-cut everything else
      // in this function, because entry.recompute() is going to recycle
      // the entry object without recomputing anything, anyway.
      return;
    }

    var key = options.makeCacheKey.apply(null, arguments);
    if (! key) {
      return fn.apply(null, arguments);
    }

    var args = [], len = arguments.length;
    while (len--) args[len] = arguments[len];

    var entry = cache.get(key);
    if (entry) {
      entry.args = args;
    } else {
      cache.set(key, entry = Entry.acquire(fn, key, args));
      entry.subscribe = options.subscribe;
      if (disposable) {
        entry.reportOrphan = reportOrphan;
      }
    }

    var value = entry.recompute();

    // Move this entry to the front of the least-recently used queue,
    // since we just finished computing its value.
    cache.set(key, entry);

    // Clean up any excess entries in the cache, but only if this entry
    // has no parents, which means we're not in the middle of a larger
    // computation that might be flummoxed by the cleaning.
    if (entry.parents.size === 0) {
      cache.clean();
    }

    // If options.disposable is truthy, the caller of wrap is telling us
    // they don't care about the result of entry.recompute(), so we should
    // avoid returning the value, so it won't be accidentally used.
    if (! disposable) {
      return value;
    }
  }

  optimistic.dirty = function () {
    var key = options.makeCacheKey.apply(null, arguments);
    if (! key) {
      return;
    }

    if (! cache.has(key)) {
      return;
    }

    cache.get(key).setDirty();
  };

  return optimistic;
}

exports.wrap = wrap;
