# frozen_string_literal: true

#
# {TDiff} adds the ability to calculate the differences between two tree-like
# objects. Simply include {TDiff} into the class which represents the tree
# nodes and define the {#tdiff_each_child} and {#tdiff_equal} methods.
#
module TDiff
  #
  # Default method which will enumerate over every child of a parent node.
  #
  # @param [Object] node
  #   The parent node.
  #
  # @yield [child]
  #   The given block will be passed each child of the parent node.
  #
  def tdiff_each_child(node,&block)
    node.each(&block) if node.kind_of?(Enumerable)
  end

  #
  # Default method which compares nodes.
  #
  # @param [Object] node
  #   A node from the new tree.
  #
  # @return [Boolean]
  #   Specifies whether the nodes are equal.
  #
  def tdiff_equal(node)
    self == node
  end

  #
  # Finds the differences between `self` and another tree.
  #
  # @param [#tdiff_each_child] tree
  #   The other tree.
  #
  # @yield [change, node]
  #   The given block will be passed the added or removed nodes.
  #
  # @yieldparam [' ', '+', '-'] change
  #   The state-change of the node.
  #
  # @yieldparam [Object] node
  #   A node from one of the two trees.
  #
  # @return [Enumerator]
  #   If no block is given, an Enumerator object will be returned.
  #
  def tdiff(tree,&block)
    return enum_for(:tdiff,tree) unless block

    # check if the nodes differ
    unless tdiff_equal(tree)
      yield '-', self
      yield '+', tree
      return self
    end

    yield ' ', self

    tdiff_recursive(tree,&block)
    return self
  end

  protected

  #
  # Recursively compares the differences between the children nodes.
  #
  # @param [#tdiff_each_child] tree
  #   The other tree.
  #
  # @yield [change, node]
  #   The given block will be passed the added or removed nodes.
  #
  # @yieldparam [' ', '+', '-'] change
  #   The state-change of the node.
  #
  # @yieldparam [Object] node
  #   A node from one of the two trees.
  #
  # @since 0.3.2
  #
  def tdiff_recursive(tree,&block)
    c = Hash.new { |hash,key| hash[key] = Hash.new(0) }
    x = enum_for(:tdiff_each_child,self)
    y = enum_for(:tdiff_each_child,tree)

    x.each_with_index do |xi,i|
      y.each_with_index do |yj,j|
        c[i][j] = if xi.tdiff_equal(yj)
                    c[i-1][j-1] + 1
                  else
                    if c[i][j-1] > c[i-1][j]
                      c[i][j-1]
                    else
                      c[i-1][j]
                    end
                  end
      end
    end

    unchanged = []
    changes = []

    x_backtrack = x.each_with_index.reverse_each
    y_backtrack = y.each_with_index.reverse_each

    next_child = lambda { |children|
      begin
        children.next
      rescue StopIteration
        # end of iteration, return a -1 index
        [nil, -1]
      end
    }

    xi, i = next_child[x_backtrack]
    yj, j = next_child[y_backtrack]

    until (i == -1 && j == -1)
      if (i != -1 && j != -1 && xi.tdiff_equal(yj))
        changes.unshift [' ', xi]
        unchanged.unshift [xi, yj]

        xi, i = next_child[x_backtrack]
        yj, j = next_child[y_backtrack]
      else
        if (j >= 0 && (i == -1 || c[i][j-1] >= c[i-1][j]))
          changes.unshift ['+', yj]

          yj, j = next_child[y_backtrack]
        elsif (i >= 0 && (j == -1 || c[i][j-1] < c[i-1][j]))
          changes.unshift ['-', xi]

          xi, i = next_child[x_backtrack]
        end
      end
    end

    # explicitly discard the c matrix
    c = nil

    # sequentially iterate over the changed nodes
    changes.each(&block)
    changes = nil

    # recurse down through unchanged nodes
    unchanged.each { |a,b| a.tdiff_recursive(b,&block) }
    unchanged = nil
  end
end
