"use strict";
// Property-based "dataflow"

var Immutable = require("immutable");
var MapSet = require("./mapset.js");

function Graph() {
  this.edgesForward = Immutable.Map();
  this.edgesReverse = Immutable.Map();
  this.damagedNodes = Immutable.Set();
  this.currentSubjectId = null;
}

Graph.prototype.withSubject = function (subjectId, f) {
  var oldSubjectId = this.currentSubjectId;
  this.currentSubjectId = subjectId;
  var result;
  try {
    result = f();
  } catch (e) {
    this.currentSubjectId = oldSubjectId;
    throw e;
  }
  this.currentSubjectId = oldSubjectId;
  return result;
};

Graph.prototype.recordObservation = function (objectId) {
  if (this.currentSubjectId) {
    this.edgesForward = MapSet.add(this.edgesForward, objectId, this.currentSubjectId);
    this.edgesReverse = MapSet.add(this.edgesReverse, this.currentSubjectId, objectId);
  }
};

Graph.prototype.recordDamage = function (objectId) {
  this.damagedNodes = this.damagedNodes.add(objectId);
};

Graph.prototype.forgetSubject = function (subjectId) {
  var self = this;
  var subjectObjects = self.edgesReverse.get(subjectId) || Immutable.Set();
  self.edgesReverse = self.edgesReverse.remove(subjectId);
  subjectObjects.forEach(function (objectId) {
    self.edgesForward = MapSet.remove(self.edgesForward, objectId, subjectId);
  });
};

Graph.prototype.repairDamage = function (repairNode) {
  var self = this;
  var repairedThisRound = Immutable.Set();
  while (true) {
    var workSet = self.damagedNodes;
    self.damagedNodes = Immutable.Set();

    var alreadyDamaged = workSet.intersect(repairedThisRound);
    if (!alreadyDamaged.isEmpty()) {
      console.warn('Cyclic dependencies involving', alreadyDamaged);
    }

    workSet = workSet.subtract(repairedThisRound);
    repairedThisRound = repairedThisRound.union(workSet);

    if (workSet.isEmpty()) break;

    workSet.forEach(function (objectId) {
      var subjects = self.edgesForward.get(objectId) || Immutable.Set();
      subjects.forEach(function (subjectId) {
        self.forgetSubject(subjectId);
        self.withSubject(subjectId, function () {
          repairNode(subjectId);
        });
      });
    });
  }
};

Graph.prototype.defineObservableProperty = function (obj, prop, value, maybeOptions) {
  var graph = this;
  var options = typeof maybeOptions === 'undefined' ? {} : maybeOptions;
  var objectId = options.objectId || '__' + prop;
  Object.defineProperty(obj, prop, {
    configurable: true,
    enumerable: true,
    get: function () {
      graph.recordObservation(objectId);
      return value;
    },
    set: function (newValue) {
      if (!options.noopGuard || !options.noopGuard(value, newValue)) {
        graph.recordDamage(objectId);
        value = newValue;
      }
    }
  });
  graph.recordDamage(objectId);
  return objectId;
};

Graph.newScope = function (o) {
  function O() {}
  O.prototype = o;
  return new O();
};

///////////////////////////////////////////////////////////////////////////

module.exports.Graph = Graph;
