class Dentaku::BulkExpressionSolver

def self.dependency_cache

def self.dependency_cache
  @dep_cache ||= {}
end

def add_dependencies(current_dependencies, variable)

def add_dependencies(current_dependencies, variable)
  node = calculator.memory[variable]
  if node.respond_to?(:dependencies)
    current_dependencies[variable] = node.dependencies
    node.dependencies.each { |d| add_dependencies(current_dependencies, d) }
  end
end

def dependencies

def dependencies
  Hash[expression_deps].tap do |d|
    d.values.each do |deps|
      unresolved = deps.reject { |ud| d.has_key?(ud) }
      unresolved.each { |u| add_dependencies(d, u) }
    end
  end
end

def expression_deps

def expression_deps
  expressions.map do |var, expr|
    [var, calculator.dependencies(expr)]
  end
end

def expression_with_exception_handler(var_name, &block)

def expression_with_exception_handler(var_name, &block)
  ->(_expr, ex) {
    ex.recipient_variable = var_name
    block.call(ex)
  }
end

def expressions

def expressions
  @expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
end

def initialize(expressions, calculator)

def initialize(expressions, calculator)
  @expression_hash = FlatHash.from_hash(expressions)
  @calculator = calculator
end

def load_results(&block)

def load_results(&block)
  facts, _formulas = expressions.transform_keys(&:downcase)
                                .transform_values { |v| calculator.ast(v) }
                                .partition { |_, v| calculator.dependencies(v, nil).empty? }
  evaluated_facts = facts.to_h.each_with_object({}) do |(var_name, ast), h|
    with_rescues(var_name, h, block) do
      h[var_name] = ast.is_a?(Array) ? ast.map(&:value) : ast.value
    end
  end
  context = calculator.memory.merge(evaluated_facts)
  variables_in_resolve_order.each_with_object({}) do |var_name, results|
    next if expressions[var_name].nil?
    with_rescues(var_name, results, block) do
      results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
        expressions[var_name],
        context.merge(results),
        &expression_with_exception_handler(var_name, &block)
      ).tap { |res|
        res.recipient_variable = var_name if res.respond_to?(:recipient_variable=)
        res
      }
    end
  end
rescue TSort::Cyclic => ex
  block.call(ex)
  {}
end

def raise_exception_handler

def raise_exception_handler
  ->(ex) { raise ex }
end

def return_undefined_handler

def return_undefined_handler
  ->(*) { :undefined }
end

def solve(&block)

def solve(&block)
  @evaluator ||= PermissiveEvaluator.new(calculator, block)
  error_handler = block || return_undefined_handler
  results = load_results(&error_handler)
  FlatHash.expand(
    expression_hash.each_with_object({}) do |(k, v), r|
      default = v.nil? ? v : :undefined
      r[k] = results.fetch(k.to_s, default)
    end
  )
end

def solve!

def solve!
  @evaluator = StrictEvaluator.new(calculator)
  solve(&raise_exception_handler)
end

def variables_in_resolve_order

def variables_in_resolve_order
  cache_key = expressions.keys.map(&:to_s).sort.join("|")
  @ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
    DependencyResolver.find_resolve_order(dependencies).tap do |d|
      self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
    end
  }
end

def with_rescues(var_name, results, block)

def with_rescues(var_name, results, block)
  yield
rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
  ex.recipient_variable = var_name
  results[var_name] = block.call(ex)
ensure
  if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
    results[var_name] = calculator.memory[var_name.downcase]
  end
end