// Copyright 2011 Google Inc. All Rights Reserved.
// Copyright 1996 John Maloney and Mario Wolczko
//
// This file is part of GNU Smalltalk.
//
// GNU Smalltalk is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 2, or (at your option) any later version.
//
// GNU Smalltalk is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// GNU Smalltalk; see the file COPYING.  If not, write to the Free Software
// Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// Translated first from Smalltalk to JavaScript, and finally to
// Dart by Google 2008-2010.
//
// Translated to Wren by Bob Nystrom 2014.

// A Wren implementation of the DeltaBlue constraint-solving
// algorithm, as described in:
//
// "The DeltaBlue Algorithm: An Incremental Constraint Hierarchy Solver"
//   Bjorn N. Freeman-Benson and John Maloney
//   January 1990 Communications of the ACM,
//   also available as University of Washington TR 89-08-06.
//
// Beware: this benchmark is written in a grotesque style where
// the constraint model is built by side-effects from constructors.
// I've kept it this way to avoid deviating too much from the original
// implementation.

// Strengths are used to measure the relative importance of constraints.
// New strengths may be inserted in the strength hierarchy without
// disrupting current constraints.  Strengths cannot be created outside
// this class, so == can be used for value comparison.
class Strength {
  construct new(value, name) {
    _value = value
    _name = name
  }

  value { _value }
  name { _name }

  nextWeaker { ORDERED[_value] }

  static stronger(s1, s2) { s1.value < s2.value }
  static weaker(s1, s2) { s1.value > s2.value }
  static weakest(s1, s2) { Strength.weaker(s1, s2) ? s1 : s2 }
  static strongest(s1, s2) { Strength.stronger(s1, s2) ? s1 : s2 }
}

// Compile time computed constants.
var REQUIRED        = Strength.new(0, "required")
var STRONG_REFERRED = Strength.new(1, "strongPreferred")
var PREFERRED       = Strength.new(2, "preferred")
var STRONG_DEFAULT  = Strength.new(3, "strongDefault")
var NORMAL          = Strength.new(4, "normal")
var WEAK_DEFAULT    = Strength.new(5, "weakDefault")
var WEAKEST         = Strength.new(6, "weakest")

var ORDERED = [
  WEAKEST, WEAK_DEFAULT, NORMAL, STRONG_DEFAULT, PREFERRED, STRONG_REFERRED
]

var ThePlanner

class Constraint {
  construct new(strength) {
    _strength = strength
  }

  strength { _strength }

  // Activate this constraint and attempt to satisfy it.
  addConstraint() {
    addToGraph()
    ThePlanner.incrementalAdd(this)
  }

  // Attempt to find a way to enforce this constraint. If successful,
  // record the solution, perhaps modifying the current dataflow
  // graph. Answer the constraint that this constraint overrides, if
  // there is one, or nil, if there isn't.
  // Assume: I am not already satisfied.
  satisfy(mark) {
    chooseMethod(mark)
    if (!isSatisfied) {
      if (_strength == REQUIRED) {
        System.print("Could not satisfy a required constraint!")
      }
      return null
    }

    markInputs(mark)
    var out = output
    var overridden = out.determinedBy
    if (overridden != null) overridden.markUnsatisfied()
    out.determinedBy = this
    if (!ThePlanner.addPropagate(this, mark)) System.print("Cycle encountered")
    out.mark = mark
    return overridden
  }

  destroyConstraint() {
    if (isSatisfied) ThePlanner.incrementalRemove(this)
    removeFromGraph()
  }

  // Normal constraints are not input constraints.  An input constraint
  // is one that depends on external state, such as the mouse, the
  // keybord, a clock, or some arbitraty piece of imperative code.
  isInput { false }
}

// Abstract superclass for constraints having a single possible output variable.
class UnaryConstraint is Constraint {
  construct new(myOutput, strength) {
    super(strength)
    _satisfied = false
    _myOutput = myOutput
    addConstraint()
  }

  // Adds this constraint to the constraint graph.
  addToGraph() {
    _myOutput.addConstraint(this)
    _satisfied = false
  }

  // Decides if this constraint can be satisfied and records that decision.
  chooseMethod(mark) {
    _satisfied = (_myOutput.mark != mark) &&
        Strength.stronger(strength, _myOutput.walkStrength)
  }

  // Returns true if this constraint is satisfied in the current solution.
  isSatisfied { _satisfied }

  markInputs(mark) {
    // has no inputs.
  }

  // Returns the current output variable.
  output { _myOutput }

  // Calculate the walkabout strength, the stay flag, and, if it is
  // 'stay', the value for the current output of this constraint. Assume
  // this constraint is satisfied.
  recalculate() {
    _myOutput.walkStrength = strength
    _myOutput.stay = !isInput
    if (_myOutput.stay) execute() // Stay optimization.
  }

  // Records that this constraint is unsatisfied.
  markUnsatisfied() {
    _satisfied = false
  }

  inputsKnown(mark) { true }

  removeFromGraph() {
    if (_myOutput != null) _myOutput.removeConstraint(this)
    _satisfied = false
  }
}

// Variables that should, with some level of preference, stay the same.
// Planners may exploit the fact that instances, if satisfied, will not
// change their output during plan execution.  This is called "stay
// optimization".
class StayConstraint is UnaryConstraint {
  construct new(variable, strength) {
    super(variable, strength)
  }

  execute() {
    // Stay constraints do nothing.
  }
}

// A unary input constraint used to mark a variable that the client
// wishes to change.
class EditConstraint is UnaryConstraint {
  construct new(variable, strength) {
    super(variable, strength)
  }

  // Edits indicate that a variable is to be changed by imperative code.
  isInput { true }

  execute() {
    // Edit constraints do nothing.
  }
}

// Directions.
var NONE = 1
var FORWARD = 2
var BACKWARD = 0

// Abstract superclass for constraints having two possible output
// variables.
class BinaryConstraint is Constraint {
  construct new(v1, v2, strength) {
    super(strength)
    _v1 = v1
    _v2 = v2
    _direction = NONE
    addConstraint()
  }

  direction { _direction }
  v1 { _v1 }
  v2 { _v2 }

  // Decides if this constraint can be satisfied and which way it
  // should flow based on the relative strength of the variables related,
  // and record that decision.
  chooseMethod(mark) {
    if (_v1.mark == mark) {
      if (_v2.mark != mark &&
          Strength.stronger(strength, _v2.walkStrength)) {
        _direction = FORWARD
      } else {
        _direction = NONE
      }
    }

    if (_v2.mark == mark) {
      if (_v1.mark != mark &&
          Strength.stronger(strength, _v1.walkStrength)) {
        _direction = BACKWARD
      } else {
        _direction = NONE
      }
    }

    if (Strength.weaker(_v1.walkStrength, _v2.walkStrength)) {
      if (Strength.stronger(strength, _v1.walkStrength)) {
        _direction = BACKWARD
      } else {
        _direction = NONE
      }
    } else {
      if (Strength.stronger(strength, _v2.walkStrength)) {
        _direction = FORWARD
      } else {
        _direction = BACKWARD
      }
    }
  }

  // Add this constraint to the constraint graph.
  addToGraph() {
    _v1.addConstraint(this)
    _v2.addConstraint(this)
    _direction = NONE
  }

  // Answer true if this constraint is satisfied in the current solution.
  isSatisfied { _direction != NONE }

  // Mark the input variable with the given mark.
  markInputs(mark) {
    input.mark = mark
  }

  // Returns the current input variable
  input { _direction == FORWARD ? _v1 : _v2 }

  // Returns the current output variable.
  output { _direction == FORWARD ? _v2 : _v1 }

  // Calculate the walkabout strength, the stay flag, and, if it is
  // 'stay', the value for the current output of this
  // constraint. Assume this constraint is satisfied.
  recalculate() {
    var ihn = input
    var out = output
    out.walkStrength = Strength.weakest(strength, ihn.walkStrength)
    out.stay = ihn.stay
    if (out.stay) execute()
  }

  // Record the fact that this constraint is unsatisfied.
  markUnsatisfied() {
    _direction = NONE
  }

  inputsKnown(mark) {
    var i = input
    return i.mark == mark || i.stay || i.determinedBy == null
  }

  removeFromGraph() {
    if (_v1 != null) _v1.removeConstraint(this)
    if (_v2 != null) _v2.removeConstraint(this)
    _direction = NONE
  }
}

// Relates two variables by the linear scaling relationship: "v2 =
// (v1 * scale) + offset". Either v1 or v2 may be changed to maintain
// this relationship but the scale factor and offset are considered
// read-only.
class ScaleConstraint is BinaryConstraint {
  construct new(src, scale, offset, dest, strength) {
    _scale = scale
    _offset = offset
    super(src, dest, strength)
  }

  // Adds this constraint to the constraint graph.
  addToGraph() {
    super()
    _scale.addConstraint(this)
    _offset.addConstraint(this)
  }

  removeFromGraph() {
    super()
    if (_scale != null) _scale.removeConstraint(this)
    if (_offset != null) _offset.removeConstraint(this)
  }

  markInputs(mark) {
    super.markInputs(mark)
    _scale.mark = _offset.mark = mark
  }

  // Enforce this constraint. Assume that it is satisfied.
  execute() {
    if (direction == FORWARD) {
      v2.value = v1.value * _scale.value + _offset.value
    } else {
      // TODO: Is this the same semantics as ~/?
      v1.value = ((v2.value - _offset.value) / _scale.value).floor
    }
  }

  // Calculate the walkabout strength, the stay flag, and, if it is
  // 'stay', the value for the current output of this constraint. Assume
  // this constraint is satisfied.
  recalculate() {
    var ihn = input
    var out = output
    out.walkStrength = Strength.weakest(strength, ihn.walkStrength)
    out.stay = ihn.stay && _scale.stay && _offset.stay
    if (out.stay) execute()
  }
}

// Constrains two variables to have the same value.
class EqualityConstraint is BinaryConstraint {
  construct new(v1, v2, strength) {
    super(v1, v2, strength)
  }

  // Enforce this constraint. Assume that it is satisfied.
  execute() {
    output.value = input.value
  }
}

// A constrained variable. In addition to its value, it maintain the
// structure of the constraint graph, the current dataflow graph, and
// various parameters of interest to the DeltaBlue incremental
// constraint solver.
class Variable {
  construct new(name, value) {
    _constraints = []
    _determinedBy = null
    _mark = 0
    _walkStrength = WEAKEST
    _stay = true
    _name = name
    _value = value
  }

  constraints { _constraints }
  determinedBy { _determinedBy }
  determinedBy=(value) { _determinedBy = value }
  mark { _mark }
  mark=(value) { _mark = value }
  walkStrength { _walkStrength }
  walkStrength=(value) { _walkStrength = value }
  stay { _stay }
  stay=(value) { _stay = value }
  value { _value }
  value=(newValue) { _value = newValue }

  // Add the given constraint to the set of all constraints that refer
  // this variable.
  addConstraint(constraint) {
    _constraints.add(constraint)
  }

  // Removes all traces of c from this variable.
  removeConstraint(constraint) {
    _constraints = _constraints.where { |c| c != constraint }
    if (_determinedBy == constraint) _determinedBy = null
  }
}

// A Plan is an ordered list of constraints to be executed in sequence
// to resatisfy all currently satisfiable constraints in the face of
// one or more changing inputs.
class Plan {
  construct new() {
    _list = []
  }

  addConstraint(constraint) {
    _list.add(constraint)
  }

  size { _list.count }

  execute() {
    for (constraint in _list) {
      constraint.execute()
    }
  }
}

class Planner {
  construct new() {
    _currentMark = 0
  }

  // Attempt to satisfy the given constraint and, if successful,
  // incrementally update the dataflow graph.  Details: If satifying
  // the constraint is successful, it may override a weaker constraint
  // on its output. The algorithm attempts to resatisfy that
  // constraint using some other method. This process is repeated
  // until either a) it reaches a variable that was not previously
  // determined by any constraint or b) it reaches a constraint that
  // is too weak to be satisfied using any of its methods. The
  // variables of constraints that have been processed are marked with
  // a unique mark value so that we know where we've been. This allows
  // the algorithm to avoid getting into an infinite loop even if the
  // constraint graph has an inadvertent cycle.
  incrementalAdd(constraint) {
    var mark = newMark()
    var overridden = constraint.satisfy(mark)
    while (overridden != null) {
      overridden = overridden.satisfy(mark)
    }
  }

  // Entry point for retracting a constraint. Remove the given
  // constraint and incrementally update the dataflow graph.
  // Details: Retracting the given constraint may allow some currently
  // unsatisfiable downstream constraint to be satisfied. We therefore collect
  // a list of unsatisfied downstream constraints and attempt to
  // satisfy each one in turn. This list is traversed by constraint
  // strength, strongest first, as a heuristic for avoiding
  // unnecessarily adding and then overriding weak constraints.
  // Assume: [c] is satisfied.
  incrementalRemove(constraint) {
    var out = constraint.output
    constraint.markUnsatisfied()
    constraint.removeFromGraph()
    var unsatisfied = removePropagateFrom(out)
    var strength = REQUIRED
    while (true) {
      for (u in unsatisfied) {
        if (u.strength == strength) incrementalAdd(u)
      }
      strength = strength.nextWeaker
      if (strength == WEAKEST) break
    }
  }

  // Select a previously unused mark value.
  newMark() { _currentMark = _currentMark + 1 }

  // Extract a plan for resatisfaction starting from the given source
  // constraints, usually a set of input constraints. This method
  // assumes that stay optimization is desired; the plan will contain
  // only constraints whose output variables are not stay. Constraints
  // that do no computation, such as stay and edit constraints, are
  // not included in the plan.
  // Details: The outputs of a constraint are marked when it is added
  // to the plan under construction. A constraint may be appended to
  // the plan when all its input variables are known. A variable is
  // known if either a) the variable is marked (indicating that has
  // been computed by a constraint appearing earlier in the plan), b)
  // the variable is 'stay' (i.e. it is a constant at plan execution
  // time), or c) the variable is not determined by any
  // constraint. The last provision is for past states of history
  // variables, which are not stay but which are also not computed by
  // any constraint.
  // Assume: [sources] are all satisfied.
  makePlan(sources) {
    var mark = newMark()
    var plan = Plan.new()
    var todo = sources
    while (todo.count > 0) {
      var constraint = todo.removeAt(-1)
      if (constraint.output.mark != mark && constraint.inputsKnown(mark)) {
        plan.addConstraint(constraint)
        constraint.output.mark = mark
        addConstraintsConsumingTo(constraint.output, todo)
      }
    }
    return plan
  }

  // Extract a plan for resatisfying starting from the output of the
  // given [constraints], usually a set of input constraints.
  extractPlanFromConstraints(constraints) {
    var sources = []
    for (constraint in constraints) {
      // if not in plan already and eligible for inclusion.
      if (constraint.isInput && constraint.isSatisfied) sources.add(constraint)
    }
    return makePlan(sources)
  }

  // Recompute the walkabout strengths and stay flags of all variables
  // downstream of the given constraint and recompute the actual
  // values of all variables whose stay flag is true. If a cycle is
  // detected, remove the given constraint and answer
  // false. Otherwise, answer true.
  // Details: Cycles are detected when a marked variable is
  // encountered downstream of the given constraint. The sender is
  // assumed to have marked the inputs of the given constraint with
  // the given mark. Thus, encountering a marked node downstream of
  // the output constraint means that there is a path from the
  // constraint's output to one of its inputs.
  addPropagate(constraint, mark) {
    var todo = [constraint]
    while (todo.count > 0) {
      var d = todo.removeAt(-1)
      if (d.output.mark == mark) {
        incrementalRemove(constraint)
        return false
      }

      d.recalculate()
      addConstraintsConsumingTo(d.output, todo)
    }

    return true
  }

  // Update the walkabout strengths and stay flags of all variables
  // downstream of the given constraint. Answer a collection of
  // unsatisfied constraints sorted in order of decreasing strength.
  removePropagateFrom(out) {
    out.determinedBy = null
    out.walkStrength = WEAKEST
    out.stay = true
    var unsatisfied = []
    var todo = [out]
    while (todo.count > 0) {
      var v = todo.removeAt(-1)
      for (constraint in v.constraints) {
        if (!constraint.isSatisfied) unsatisfied.add(constraint)
      }

      var determining = v.determinedBy
      for (next in v.constraints) {
        if (next != determining && next.isSatisfied) {
          next.recalculate()
          todo.add(next.output)
        }
      }
    }

    return unsatisfied
  }

  addConstraintsConsumingTo(v, coll) {
    var determining = v.determinedBy
    for (constraint in v.constraints) {
      if (constraint != determining && constraint.isSatisfied) {
        coll.add(constraint)
      }
    }
  }
}

var total = 0

// This is the standard DeltaBlue benchmark. A long chain of equality
// constraints is constructed with a stay constraint on one end. An
// edit constraint is then added to the opposite end and the time is
// measured for adding and removing this constraint, and extracting
// and executing a constraint satisfaction plan. There are two cases.
// In case 1, the added constraint is stronger than the stay
// constraint and values must propagate down the entire length of the
// chain. In case 2, the added constraint is weaker than the stay
// constraint so it cannot be accomodated. The cost in this case is,
// of course, very low. Typical situations lie somewhere between these
// two extremes.
var chainTest = Fn.new {|n|
  ThePlanner = Planner.new()
  var prev = null
  var first = null
  var last = null

  // Build chain of n equality constraints.
  for (i in 0..n) {
    var v = Variable.new("v", 0)
    if (prev != null) EqualityConstraint.new(prev, v, REQUIRED)
    if (i == 0) first = v
    if (i == n) last = v
    prev = v
  }

  StayConstraint.new(last, STRONG_DEFAULT)
  var edit = EditConstraint.new(first, PREFERRED)
  var plan = ThePlanner.extractPlanFromConstraints([edit])
  for (i in 0...100) {
    first.value = i
    plan.execute()
    total = total + last.value
  }
}

var change = Fn.new {|v, newValue|
  var edit = EditConstraint.new(v, PREFERRED)
  var plan = ThePlanner.extractPlanFromConstraints([edit])
  for (i in 0...10) {
    v.value = newValue
    plan.execute()
  }

  edit.destroyConstraint()
}

// This test constructs a two sets of variables related to each
// other by a simple linear transformation (scale and offset). The
// time is measured to change a variable on either side of the
// mapping and to change the scale and offset factors.
var projectionTest = Fn.new {|n|
  ThePlanner = Planner.new()
  var scale = Variable.new("scale", 10)
  var offset = Variable.new("offset", 1000)
  var src = null
  var dst = null

  var dests = []
  for (i in 0...n) {
    src = Variable.new("src", i)
    dst = Variable.new("dst", i)
    dests.add(dst)
    StayConstraint.new(src, NORMAL)
    ScaleConstraint.new(src, scale, offset, dst, REQUIRED)
  }

  change.call(src, 17)
  total = total + dst.value
  if (dst.value != 1170) System.print("Projection 1 failed")

  change.call(dst, 1050)

  total = total + src.value
  if (src.value != 5) System.print("Projection 2 failed")

  change.call(scale, 5)
  for (i in 0...n - 1) {
    total = total + dests[i].value
    if (dests[i].value != i * 5 + 1000) System.print("Projection 3 failed")
  }

  change.call(offset, 2000)
  for (i in 0...n - 1) {
    total = total + dests[i].value
    if (dests[i].value != i * 5 + 2000) System.print("Projection 4 failed")
  }
}

var start = System.clock
for (i in 0...40) {
  chainTest.call(100)
  projectionTest.call(100)
}

System.print(total)
System.print("elapsed: %(System.clock - start)")
