# -*- ruby encoding: utf-8 -*-

# Represents a simplistic (non-contextual) change. Represents the removal or
# addition of an element from either the old or the new sequenced
# enumerable.
class Diff::LCS::Change
  # The only actions valid for changes are '+' (add), '-' (delete), '='
  # (no change), '!' (changed), '<' (tail changes from first sequence), or
  # '>' (tail changes from second sequence). The last two ('<>') are only
  # found with Diff::LCS::diff and Diff::LCS::sdiff.
  VALID_ACTIONS = %W(+ - = ! > <)

  def self.valid_action?(action)
    VALID_ACTIONS.include? action
  end

  # Returns the action this Change represents.
  attr_reader :action

  # Returns the position of the Change.
  attr_reader :position
  # Returns the sequence element of the Change.
  attr_reader :element

  def initialize(*args)
    @action, @position, @element = *args

    unless Diff::LCS::Change.valid_action?(@action)
      raise "Invalid Change Action '#{@action}'"
    end
    raise "Invalid Position Type" unless @position.kind_of? Fixnum
  end

  def inspect
    to_a.inspect
  end

  def to_a
    [ @action, @position, @element ]
  end

  def self.from_a(arr)
    arr = arr.flatten(1)
    case arr.size
    when 5
      Diff::LCS::ContextChange.new(*(arr[0...5]))
    when 3
      Diff::LCS::Change.new(*(arr[0...3]))
    else
      raise "Invalid change array format provided."
    end
  end

  include Comparable

  def ==(other)
    (self.action == other.action) and
    (self.position == other.position) and
    (self.element == other.element)
  end

  def <=>(other)
    r = self.action <=> other.action
    r = self.position <=> other.position if r.zero?
    r = self.element <=> other.element if r.zero?
    r
  end

  def adding?
    @action == '+'
  end

  def deleting?
    @action == '-'
  end

  def unchanged?
    @action == '='
  end

  def changed?
    @action == '!'
  end

  def finished_a?
    @action == '>'
  end

  def finished_b?
    @action == '<'
  end
end

# Represents a contextual change. Contains the position and values of the
# elements in the old and the new sequenced enumerables as well as the action
# taken.
class Diff::LCS::ContextChange < Diff::LCS::Change
  # We don't need these two values.
  undef :position
  undef :element

  # Returns the old position being changed.
  attr_reader :old_position
  # Returns the new position being changed.
  attr_reader :new_position
  # Returns the old element being changed.
  attr_reader :old_element
  # Returns the new element being changed.
  attr_reader :new_element

  def initialize(*args)
    @action, @old_position, @old_element, @new_position, @new_element = *args

    unless Diff::LCS::Change.valid_action?(@action)
      raise "Invalid Change Action '#{@action}'"
    end
    unless @old_position.nil? or @old_position.kind_of? Fixnum
      raise "Invalid (Old) Position Type"
    end
    unless @new_position.nil? or @new_position.kind_of? Fixnum
      raise "Invalid (New) Position Type"
    end
  end

  def to_a
    [ @action,
      [ @old_position, @old_element ],
      [ @new_position, @new_element ]
    ]
  end

  def inspect(*args)
    to_a.inspect
  end

  def self.from_a(arr)
    Diff::LCS::Change.from_a(arr)
  end

  # Simplifies a context change for use in some diff callbacks. '<' actions
  # are converted to '-' and '>' actions are converted to '+'.
  def self.simplify(event)
    ea = event.to_a

    case ea[0]
    when '-'
      ea[2][1] = nil
    when '<'
      ea[0] = '-'
      ea[2][1] = nil
    when '+'
      ea[1][1] = nil
    when '>'
      ea[0] = '+'
      ea[1][1] = nil
    end

    Diff::LCS::ContextChange.from_a(ea)
  end

  def ==(other)
    (@action == other.action) and
    (@old_position == other.old_position) and
    (@new_position == other.new_position) and
    (@old_element == other.old_element) and
    (@new_element == other.new_element)
  end

  def <=>(other)
    r = @action <=> other.action
    r = @old_position <=> other.old_position if r.zero?
    r = @new_position <=> other.new_position if r.zero?
    r = @old_element <=> other.old_element if r.zero?
    r = @new_element <=> other.new_element if r.zero?
    r
  end
end
