#!/usr/bin/ruby -w

# TODO: optimize creation of object of Frame class and subclassess.
# Implement Frame.build with user-friendly interface (pass host objects),
# and Frame.new with direct interface, specifing strings.
# What about validating input? Maybe do not validate, as wrong input
# does not pose security risk, only causes incorrect results.

# FIXME: implement all TODOs
# TODO: optimize sniffing mode - use dynamic bpf filters which could
# be changed on runtime (i.e. with a command "setfilter" - like "start",
# "stop")

require 'optparse'
require 'timeout'
require 'socket'

# Simple debug routine
def debug(text)
  t = Time.now
  STDERR.puts "#{t.tv_sec}.#{t.tv_usec} debug: " + text if EB_DEBUG
end

def fatal(text)
  STDERR.puts "fatal: " + text
end

def warning(text)
  STDERR.puts "warning: " + text
end

# All tests should be subclasses of TestGeneric
class TestGeneric
public
  def result
    ((defined? @finished) && (defined? @result)) ? @result : nil
  end

  def get_flag(f)
    @flags = {} if ! defined? @flags
    @flags[f]
  end

# Methods only for use in test_loop and internally.
private
  def set_flag(f)
    @flags = {} if ! defined? @flags
    @flags[f] = true
  end

  def failed(text)
    STDERR.puts "test-failed: (#{self.class}) #{text}"
  end
  def debug(text)
    super("(#{self.class}) #{text}")
  end
  def fatal(text)
    super("(#{self.class}) #{text}")
  end
  def warning(text)
    super("(#{self.class}) #{text}")
  end
  # Class method variant of warning method
  def self.warning(text)
    super("(#{self}) #{text}")
  end


  # Injects given frame. After the frame is sniffed test_loop yields
  # with state equals to 'state_after_inject'.
  # If frame can't be sniffed after 2 seconds raises exception.
  # Uses the same timer as set_timer and can't be used in the same time
  # as that method.
  def inject(frame, state_after_inject)
    raise "Can't inject when timer set" if @timeout_type == :wait
    Injector.create.inject frame.to_s

    @timeout = 2
    @timeout_type = :inject
    @timeout_just_set = true

    @inject_frame = frame
    @inject_state_after = state_after_inject
  end

  # In the next yield test_loop will set state to given value.
  # Next yield begins after frame was sniffed or timeout expired,
  # so if you don't want to wait use fast_goto.
  # This method has lower priority than events caused by timer or inject,
  # so these events could replace set state.
  def goto(state)
    @next_state = state
    @fast_goto = false
  end

  # Same as goto but skips frame sniffing. Has higher priority than
  # events caused by timer and inject, so these events can't overwrite
  # set state.
  def fast_goto(state)
    @next_state = state
    @fast_goto = true
  end

  # Goes to specified state after specified time passes.
  # Does not interrupt running test_loop yield-block - if time passes
  # when the block is executed the state is changed in next iteration.
  # Can be cancelled with stop_timer.
  # Uses same timer as inject, so can't be used with that method at the
  # same time.
  # When timeout event occur state set by goto is overwritten.
  def set_timer(timeout, state_after_timeout)
    raise "Can't set timer when injecting frame" if @timeout_type==:inject
    @timeout = timeout
    @timeout_type = :wait
    @timeout_just_set = true
    @wait_state_after = state_after_timeout
  end

  # Cancels timeout specified when calling set_timer and inject.
  def stop_timer
    @timeout = @timeout_type = @timeout_just_set = nil
  end

  # Causes test_loop to stop yielding and ends test gracefully,
  # i.e. stops sniffer.
  def finish_test
    @finished = true
  end

  # Starts sniffing and yields every frame along with current state to
  # given block.
  # For the first iteraion state is :start and frame is nil.
  # If state is not changed with [fast_]goto, inject or set_timer it
  # remains the same thru iterations.
  # If timeout set by set_timer expires frame is nil.
  # NOTE: Do not use break in yield block as it causes the sniffer
  # to remain running. Use finish_test method instead.
  #
  # TODO: Is there any way to make this method less complicated?
  def test_loop
    sniffer = Sniffer.create
    sniffer.start

    @inject_frame = nil
    stop_timer
    goto :start
    frame = nil
    loop_time = 0

    loop do
      begin
        # Get time for loop time computation and yield frame and state
        before_loop = Time.now
        debug "state: #{@next_state}"
        yield frame, @next_state
        after_yield = Time.now
        break if defined? @finished # set by finish_test

        # Decrement timeout if set
        if @timeout.nil?
          wait_time = 0
        else
          if ! @timeout_just_set
            @timeout -= loop_time
            # Raise timeout only if no fast_goto is performed at the
            # moment. Fast goto shouldn't be interrupted.
            raise Timeout::Error if @timeout <= 0 && ! @fast_goto
          end
          wait_time = @timeout
        end

        # Skip frame sniffing when doing fast_goto
        if @fast_goto
          @fast_goto = false
          debug "state changed by fast_goto"
        else
          if wait_time == 0
            warning "No timeout set, test logic error? " \
                    "(state: #{@next_state})"
          end
          # Sniff one frame - if wait_time == 0 wait forever
          timeout(wait_time) { frame = sniffer.next_frame }
          debug "frame: #{frame}"
        end

      rescue Timeout::Error
        # Handle timeout differently depending on operation performed
        case @timeout_type
        when :wait
          @next_state = @wait_state_after
          frame = nil
        when :inject
          raise "Can't sniff injected frame"
        else
          raise "Timeout when no timer set -- should not happen"
        end

        stop_timer
      end

      # Check if injected frame was sniffed (if it was injected)
      if @timeout_type == :inject && @inject_frame && \
         frame == @inject_frame && frame.direction == :out
        @next_state = @inject_state_after
        @inject_frame = nil
        stop_timer
      end

      # How much time was spent in this loop iteration
      after_loop = Time.now
      if @timeout_just_set
        loop_time = after_loop - after_yield
        @timeout_just_set = nil
      else
        loop_time = after_loop - before_loop
      end
    end

    sniffer.stop
  end

  # FIXME: somehow detect duplicates of sent frames
  def test_jabber(frame)
    debug "Testing jabber:"
    debug "> #{frame}"
    ret = true
    if frame.enet_src == @hy.mac && frame.direction == :in
      failed "y (#{@hy.name}) is jabbering"
    elsif frame.enet_src == @hy.mac && frame.direction == :out
      failed "This machine is sending frames with y (#{@hy.name}) src mac"
    elsif frame.class == ArpReq && frame.arp_tip == @hy.ip && \
          frame.direction == :in
      failed "Someone is talking to y (#{@hy.name}), it _will_ reply"
    elsif frame.class == ArpReq && frame.arp_tip == @hy.ip && \
          frame.direction == :out
      failed "This machine is talking to y (#{@hy.name}), it _will_ reply"
    elsif frame.enet_dst == @hy.mac && frame.direction == :in
      failed "Someone is talking to y (#{@hy.name}), it _may_ reply"
    elsif frame.enet_dst == @hy.mac && frame.direction == :out
      failed "This machine is talking to y (#{@hy.name})"
    else
      debug "Not harmful jabber"
      ret = false
    end
    set_flag(:jabber) if ret
    ret
  end
end

class MacResolver < TestGeneric
  def initialize(h0)
    @h0 = h0
  end
  # Updates mac of given host by resolving it with ARP Request.
  # If resolving was successfull returns true, false otherwise.
  def resolve!(host)
    req = ArpReq.new(@h0, Host.broadcast, @h0, @h0, Host.null, host)
    success = false
    test_loop do |frame, state|
      case state
      when :start
        inject(req, :ack)
        goto :idle
      when :ack
        set_timer(RTT, :no_reply)
        goto :wait_for_reply
      when :wait_for_reply
        if frame.class == ArpReply && \
           frame.enet_dst == @h0.mac && \
           frame.arp_sip == host.ip && \
           frame.arp_tmac == @h0.mac && \
           frame.arp_tip == @h0.ip
          host.set_mac(frame.arp_smac)
          success = true
          finish_test
        end
      when :no_reply
        finish_test
      when :idle
        # nop
      else
        raise "Unknown state: '#{state}'"
      end
    end
    success
  end
end

class TestA < TestGeneric
  def perform
    # Prepare frames
    p1a = ArpReq.new(@h0, Host.broadcast, @h0, @h0, Host.null, @hy)
    p1a_reply = ArpReply.asym_reply_to(p1a, @hy)
    p1b = ArpReq.new(@h0, Host.broadcast, @h0, @h0, Host.null, @hx)
    p1b_reply = ArpReply.asym_reply_to(p1b, @hx)
    p2 = Frame.new(@hy, @hx, "IP")
    p3 = ArpReq.new(@h0, Host.broadcast, PAUSE, @h0, Host.null, @hy)
    p3_asym_reply = ArpReply.asym_reply_to(p3, @hy)
    p3_sym_reply = ArpReply.sym_reply_to(p3, @hy)
    p4 = ArpReq.new(@h0, @hx, @hy, @hy, Host.null, @hx)
    p4_asym_reply = ArpReply.asym_reply_to(p4, @hx)
    p4_sym_reply = ArpReply.sym_reply_to(p4, @hx)
    p5 = ArpReq.new(@h0, Host.broadcast, @hy, @h0, Host.null, @hy)
    p5_asym_reply = ArpReply.asym_reply_to(p5, @hy)
    p5_sym_reply = ArpReply.sym_reply_to(p5, @hy)
    p6 = p4
    p6_asym_reply = p4_asym_reply

    fix = ArpReq.new(@hb, Host.broadcast, @h0, @h0, Host.null, @hy)
    fix_asym_reply = ArpReply.asym_reply_to(fix, @hy)
    fix_sym_reply = ArpReply.sym_reply_to(fix, @hy)

    test_loop do |frame, state|
      case state
      when :start
        inject(p1a, :p1a_inject_ack)
        goto :idle
      when :p1a_inject_ack
        set_timer(RTT, :p1a_no_reply)
        goto :p1a_wait_for_reply
      when :p1a_wait_for_reply
        if frame == p1a_reply
          debug "OK: Reply to p1a"
          stop_timer
          fast_goto :p1b_start
        else
          finish_test if test_jabber(frame)
        end
      when :p1a_no_reply
        fatal "No reply to p1a"
        set_flag(:stop)
        finish_test

      when :p1b_start
        inject(p1b, :p1b_inject_ack)
        goto :idle
      when :p1b_inject_ack
        set_timer(RTT, :p1b_no_reply)
        goto :p1b_wait_for_reply
      when :p1b_wait_for_reply
        if frame == p1b_reply
          debug "OK: Reply to p1b"
          stop_timer
          fast_goto :p2_start
        else
          finish_test if test_jabber(frame)
        end
      when :p1b_no_reply
        set_flag(:stop)
        finish_test

      when :p2_start
        inject(p2, :p2_inject_ack)
        goto :idle
      when :p2_inject_ack
        set_timer(OWT, :p3_start)
        goto :idle

      when :p3_start
        inject(p3, :p3_inject_ack)
        goto :idle
      when :p3_inject_ack
        set_timer(RTT, :p3_no_reply)
        goto :p3_wait_for_reply
      when :p3_wait_for_reply
        case frame
        when p3_asym_reply
          fatal "Asymetric reply to p3. FIXME: maybe can be recovered"
          stop_timer
          set_flag(:stop)
          fast_goto :fix_start
        when p3_sym_reply
          fatal "Symetric reply to p3"
          stop_timer
          set_flag(:stop)
          fast_goto :fix_start
        else
          fast_goto :fix_start if test_jabber(frame)
        end
      when :p3_no_reply
        debug "OK: No reply to p3"
        fast_goto :p4_start

      when :p4_start
        inject(p4, :p4_inject_ack)
        goto :idle
      when :p4_inject_ack
        set_timer(RTT, :p4_no_reply)
        goto :p4_wait_for_reply
      when :p4_wait_for_reply
        case frame
        when p4_asym_reply
          debug "OK: Asymetric reply to p4"
          stop_timer
          fast_goto :p5_start
        when p4_sym_reply
          fatal "Symetric reply to p4"
          stop_timer
          set_flag(:stop)
          fast_goto :fix_start
        else
          fast_goto :fix_start if test_jabber(frame)
        end
      when :p4_no_reply
        debug "OK: No reply to p4"
        @result = 0
        fast_goto :fix_start

      when :p5_start
        inject(p5, :p5_inject_ack)
        goto :idle
      when :p5_inject_ack
        set_timer(RTT, :p5_no_reply)
        goto :p5_wait_for_reply
      when :p5_wait_for_reply
        case frame
        when p5_asym_reply
          debug "OK: Asymetric reply to p5"
          stop_timer
          @result = 0
          fast_goto :fix_start
        when p5_sym_reply
          fatal "Symetric reply to p5"
          stop_timer
          set_flag(:stop)
          fast_goto :fix_start
        else
          fast_goto :fix_start if test_jabber(frame)
        end
      when :p5_no_reply
        debug "OK: No reply to p5"
        warning "Test not reliable if host #{@hy.name} is running Windows"
        fast_goto :p6_start

      when :p6_start
        inject(p6, :p6_inject_ack)
        goto :idle
      when :p6_inject_ack
        set_timer(RTT, :p6_no_reply)
        goto :p6_wait_for_reply
      when :p6_wait_for_reply
        case frame
        when p6_asym_reply
          debug "OK: Asymetric reply to p6"
          if OPTIMISTIC
            warning "Optimistic mode used"
            @result = 1
          else
            @result = nil
          end
          stop_timer
          fast_goto :fix_start
        when p6_sym_reply
          fatal "Symetric reply to p6"
          stop_timer
          set_flag(:stop)
          fast_goto :fix_start
        else
          fast_goto :fix_start if test_jabber(frame)
        end
      when :p6_no_reply
        debug "OK: No reply to p6"
        @result = 0
        fast_goto :fix_start

      # fix phase
      when :fix_start
        # Fix is invoked from different places, maybe there is
        # timer running (if so this is bug in test logic!).
        stop_timer
        inject(fix, :fix_inject_ack)
        goto :fix_idle
      when :fix_idle
        # nop
      when :fix_inject_ack
        debug "OK: fix injected"
        set_timer(RTT, :fix_no_reply)
        goto :fix_wait_for_reply
      when :fix_wait_for_reply
        case frame
        when fix_asym_reply
          debug "OK: Asymetric reply to fix"
          finish_test
        when fix_sym_reply
          fatal "Symetric reply to fix"
          set_flag(:stop)
          finish_test
        else
          # Do not check other frames for jabber
          # as it may result in endless loop
        end
      when :fix_no_reply
        fatal "No reply to fix"
        set_flag(:stop)
        finish_test

      when :idle
        fast_goto :fix_start if test_jabber(frame)
      else
        raise "Unknown state: '#{state}'"
      end
    end
  end
end

class TestA1 < TestA
  def initialize(h0, h1, h2, hb)
    @h0, @hb = h0, hb
    @hx, @hy = h1, h2
  end
end

class TestA2 < TestA
  def initialize(h0, h1, h2, hb)
    @h0, @hb = h0, hb
    @hx, @hy = h2, h1
  end
end

# TODO: Create detailed test documentation
class TestB < TestGeneric
  def perform
    # Prepare frames
    p1 = ArpReq.new(@hb, @hy, @hx, @hx, Host.null, @hy)
    p1_reply_asym = ArpReply.asym_reply_to(p1, @hy)
    p1_reply_sym = ArpReply.sym_reply_to(p1, @hy)
    p2 = Frame.new(@hy, @hb, "IP") # zero sized IP
    p3 = ArpReq.new(@h0, @hx, @hy, @hy, Host.null, @hx)
    # TODO: Fix ARP tables in case of symetric ARP replies
    # TODO: Does Arp Request fix arp tables, or we need Arp Reply?
    p3_reply_asym = ArpReply.asym_reply_to(p3, @hx)
    p3_reply_sym = ArpReply.sym_reply_to(p3, @hx)
    p3a = ArpReq.new(@h0, @hy, @h0, @h0, Host.null, @hy)
    # In following case sym == asum
    p3a_reply = ArpReply.asym_reply_to(p3a, @hy)
    fix = ArpReq.new(@h0, Host.broadcast, @h0, @h0, Host.null, @hy)
    # In following case sym == asum
    fix_reply = ArpReply.asym_reply_to(fix, @hy)

    test_loop do |frame, state|
      case state
      when :start
        inject(p1, :p1_inject_ack)
        goto :idle

      when :p1_inject_ack
        set_timer(RTT, :p2_start)
        goto :p1_wait_for_reply
      when :p1_wait_for_reply
        case frame
          when p1_reply_asym
            failed "Network delivered reply to p1"
            set_flag(:stop)
            finish_test # No need to fix switches
          when p1_reply_sym
            fatal "Symetric reply to p1"
            set_flag(:stop)
            finish_test # No need to fix switches
          else
            fast_goto :fix_start if test_jabber(frame)
        end

      when :p2_start
        inject(p2, :p2_inject_ack)
        goto :idle
      when :p2_inject_ack
        set_timer(OWT, :p3_start)
        goto :idle

      when :p3_start
        inject(p3, :p3_inject_ack)
        goto :idle
      when :p3_inject_ack
        set_timer(RTT, :p3_no_reply)
        goto :p3_wait_for_reply
      when :p3_wait_for_reply
        case frame
        when p3_reply_asym
          debug "OK: Asymetric reply to p3"
          @result = 1
          stop_timer
          fast_goto :p3a_start
        when p3_reply_sym
          fatal "Symetric reply to p3"
          set_flag(:stop)
          stop_timer
          fast_goto :fix_start
        else
          fast_goto :fix_start if test_jabber(frame)
        end
      when :p3_no_reply
        debug "OK: No reply to p3"
        @result = 0
        fast_goto :p3a_start

      when :p3a_start
        inject(p3a, :p3a_inject_ack)
        goto :idle
      when :p3a_inject_ack
        set_timer(RTT, :p3a_no_reply)
        goto :p3a_wait_for_reply
      when :p3a_wait_for_reply
        if frame == p3a_reply
          failed "Reply to p3a"
          # Result caused by jabber with very high probability
          # Set jabber flag to allow repetiion
          set_flag(:jabber)
          # Overwrite previously set result
          @result = nil
          # No need to fix switches - p3a reply has already done so
          finish_test
        else
          if test_jabber(frame)
            # Overwrite previously set result
            @result = nil
            fast_goto :fix_start
          end
        end
      when :p3a_no_reply
        debug "OK: No reply to p3a"
        fast_goto :fix_start

      # fix phase
      when :fix_start
        # Fix is invoked from different places, maybe there is
        # timer running (if so this is bug in test logic!).
        stop_timer
        inject(fix, :fix_inject_ack)
        goto :fix_idle
      when :fix_idle
        # nop
      when :fix_inject_ack
        debug "OK: fix injected"
        set_timer(RTT, :fix_no_reply)
        goto :fix_wait_for_reply
      when :fix_wait_for_reply
        case frame
        when fix_reply
          debug "OK: ARP reply to fix"
          finish_test
        else
          # Do not check other frames for jabber
          # as it may result in endless loop
        end
      when :fix_no_reply
        fatal "No reply to fix"
        set_flag(:stop)
        finish_test

      when :idle
        fast_goto :fix_start if test_jabber(frame)

      else
        raise "Unknown state: '#{state}'"
      end
    end
  end
end

class TestB1 < TestB
  def initialize(h0, h1, h2, hb)
    @h0, @hb = h0, hb
    @hx, @hy = h1, h2
  end
end

class TestB2 < TestB
  def initialize(h0, h1, h2, hb)
    @h0, @hb = h0, hb
    @hx, @hy = h2, h1
  end
end

# FIXME: Fix this kludgy mess
class Host
  attr_reader :ip, :mac
  attr_accessor :name

  def initialize(raw_ip, raw_mac)
    set_ip(raw_ip)
    set_mac(raw_mac)
    @name = "?"
  end

  # Validate and rewrite ip
  def set_ip(ip)
    if ip.nil?
      @ip = nil
    else
      t = ip.split "."
      raise "Invalid IP address length: #{t.length}" if t.length != 4
      @ip = ""
      t.each do |s|
        i = s.to_i
        raise "Invalid IP address" if i < 0 || i > 255
        @ip += "#{i}."
      end
      @ip.chop! # Remove the last dot
    end
  end

  # Validate and rewrite mac
  def set_mac(mac)
    if mac.nil?
      @mac = nil
    else
      t = mac.split ":"
      raise "Invalid MAC address length: #{t.length}" if t.length != 6
      @mac = ""
      t.each do |s|
        i = s.to_i(16)
        raise "Invalid MAC address" if i < 0 || i > 255
        @mac += sprintf("%.2x:", i)
      end
      @mac.chop! # Remove the last colon
    end
  end

  def self.create_self(device)
    # Get mac
    begin
      io_SIOCGIFHWADDR = 0x8927 # magic number
      sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM,0)
      b = [device,""].pack('a16h16')
      sock.ioctl(io_SIOCGIFHWADDR, b)
      sock.close
    rescue SystemCallError
      raise "Couldn't get MAC address of device #{device}"
    end

    mac = sprintf("%.2x:%.2x:%.2x:%.2x:%.2x:%.2x",
          b[18], b[19], b[20], b[21], b[22], b[23])

    begin
      io_SIOCGIFADDR = 0x8915 # another magic number
      sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM,0)
      b = [device,""].pack('a16h16')
      sock.ioctl(io_SIOCGIFADDR, b);
      sock.close
    rescue SystemCallError
      raise "Couldn't get IP address of device #{device}"
    end

    ip = sprintf("%d.%d.%d.%d", b[20], b[21], b[22], b[23])

    self.new(ip, mac)
  end

  def self.broadcast
    self.new("255.255.255.255", "ff:ff:ff:ff:ff:ff")
  end

  def self.null
    self.new("0.0.0.0", "00:00:00:00:00:00")
  end

  def self.new_mac(mac)
    self.new(nil, mac)
  end

  def self.new_ip(ip)
    self.new(ip, nil)
  end

  def to_s
    (@ip == nil ? "ip_unset" : @ip) + " (" + \
    (@mac == nil ? "mac_unset" : @mac) + ")"
  end
end

class TestSequence
  def initialize(host_0, host_1, host_2, host_b)
    @host_0 = host_0
    @host_1 = host_1
    @host_2 = host_2
    @host_b = host_b
    @test_types = [TestA1, TestA2, TestB1, TestB2]
    @tests = {}
    @test_seq = []
  end

  # Returns true when entire test sequence was performed successfully,
  # nil otherwise
  def perform(repeat_count)
    raise "Repeat count < 1" if repeat_count < 1

    @test_types.each do |test_class|
      t = nil
      repeat_count.times do |i|
        if t
          warning "Repeating test #{test_class}, " + \
                  "try #{i+1} of #{repeat_count}"
        end
        t = test_class.new(@host_0, @host_1, @host_2, @host_b)
        t.perform
        @test_seq << t
        return nil if t.get_flag(:stop)
        break if ! t.get_flag(:jabber)
      end
      @tests[test_class] = t
    end
    true
  end

  def raw_results
    t_old = nil
    str = ""
    @test_seq.each do |t|
      str += " " if t.class != t_old.class
      str += (t.result ? t.result.to_s : '?')
      t_old = t
    end
    str.strip
  end

  def results
    r = [@tests[TestA1].result, @tests[TestA2].result]

    if @tests[TestB1].result == @tests[TestB2].result
      # OK
      r << @tests[TestB1].result
    elsif @tests[TestB1].result != @tests[TestB2].result && \
          @tests[TestB1].result != nil && @tests[TestB2].result != nil
      warning "B1 and B2 results differ, assuming B test useless"
      # FIXME: If B1 != B2 then assuming B was not successfull may be
      # not enough
      r << nil
    elsif @tests[TestB1].result != nil && @tests[TestB2].result == nil
      warning "B1 passed, B2 not passed -- using B1"
      r << @tests[TestB1].result
    elsif @tests[TestB2].result != nil && @tests[TestB1].result == nil
      warning "B2 passed, B1 not passed -- using B2"
      r << @tests[TestB2].result
    elsif @tests[TestB1].result == nil && @tests[TestB2].result == nil
      r << nil
    else
      fatal "Results not expected: B1=#{@tests[TestB1].result} " + \
            "B2=#{@tests[TestB2].result}"
      r << nil
    end

    mask = 0
    r.each_with_index do |v, i|
      mask += 2**(2-i) if v == nil
    end
    cons = 0
    r.each_with_index do |v, i|
      cons += v*2**(2-i) if v != nil
    end

    possible = []
    (0..7).each do |v|
      possible << ((v & mask) | cons)
    end

    possible.uniq!

    layouts = {}
    layouts[4] = 1
    layouts[7] = 2
    layouts[2] = 3
    layouts[5] = 4
    layouts[0] = 5
    layouts[3] = 6
    layouts[6] = 7
    layouts[1] = 8

    # FIXME: Rethink following
    trees = {}
    trees[1]  = " 0 1 2\n"
    trees[1] += " | | |\n"
    trees[1] += " *~*~*"

    trees[2]  = " 1 0 2\n"
    trees[2] += " | | |\n"
    trees[2] += " *~*~*"

    trees[3]  = " 0 2 1\n"
    trees[3] += " | | |\n"
    trees[3] += " *~*~*"

    trees[4]  = " 0   1 2\n"
    trees[4] +="  \\ /  |\n"
    trees[4] += "   *-~-*"

    trees[5]  = " 1   2 0\n"
    trees[5] +="  \\ /  |\n"
    trees[5] += "   *-~-*"

    trees[6]  = " 0   2 1\n"
    trees[6] +="  \\ /  |\n"
    trees[6] += "   *-~-*"

    trees[7]  = "   1\n"
    trees[7] += "   |\n"
    trees[7] += " 0 * 2\n"
    trees[7] += " | : |\n"
    trees[7] += " *~*~*"

    trees[8]  = " 0 1 2\n"
    trees[8] +="  \\|/\n"
    trees[8] += "   *"

    ret = []
    possible.each { |p| ret << trees[layouts[p]] }
    ret
  end
end

# Everything that prints /^OK/ on startup and understands "exit" command
class WireProc
  def initialize(cmd)
    @proc = IO.popen(cmd, "r+")
    begin
      if @proc.readline !~ /^OK/
        raise "Starting of subprocess failed"
      end
    rescue EOFError
      raise "Starting of subprocess failed"
    end
  end

  def close
    @proc.puts "exit"
    @proc.close
  end
end

class Injector < WireProc
  private_class_method :new
  @@injector = nil

  def initialize(device_name)
    super "eb-injector #{device_name}"
  end

  def self.create(device_name = nil)
    @@injector = new(device_name) unless @@injector
    @@injector
  end

  def inject(f_text)
    @proc.puts(f_text)
  end
end

class Sniffer < WireProc
  private_class_method :new
  @@sniffer = nil

  def initialize(device_name)
    super "eb-sniffer #{device_name}"
    @status = :stopped
  end

  def self.create(device_name = nil)
    @@sniffer = new(device_name) unless @@sniffer
    @@sniffer
  end

  # Start sniffing, will block until acked
  def start
    if started?
      raise "Sniffing already started"
    end

    @proc.puts "start"
    @proc.flush

    line = ""
    begin
      timeout(2) { line = @proc.readline }
    rescue Timeout::Error
      raise "Timeout waiting for sniffer start acknowledge"
    end
    if line =~ /^OK Sniffing started/
      debug "Sniffing started"
      @status = :started
    else
      raise "Sniffing start failed"
    end
  end
  def stop
    if ! started?
      raise "Sniffing already stopped"
    end

    @proc.puts "stop"
    # Flush frames already sniffed which have staled in sniffer-ruby buffer
    # and wait for ack
    begin
      timeout(2) do
        loop { break if @proc.readline =~ /^OK Sniffing stopped/ }
      end
    rescue Timeout::Error
      raise "Timeout waiting for sniffing stop acknowledge"
    end
    debug "Sniffing stopped"

    @status = :stopped
  end
  def started?
    @status == :started
  end

  # Return next frame from sniffer
  def next_frame
    return FrameFactory.create(@proc.readline)
  end
end

# Must be defined before Frame subclasses
class FrameFactory
  private_class_method :new # Don't allow for object creation
  @@frame_types = []
  # Register given frame types. Accept only 'Frame' subclasses.
  def self.register(cl)
    raise ArgumentError if ! (cl < Frame) || cl == Frame
    if ! (cl.respond_to? :likeness)
      raise ArgumentError, "#{cl} doesn't respond to likeness method"
    end
    @@frame_types << cl
  end

  # Creates new frame based on textual representation
  # If multiple frame class matched create most specific one
  def self.create(text)
    f = text.split

    # Compute likeness points for every registered frame class
    candidates = {}
    @@frame_types.each do |t|
      candidates[t] = t.likeness(f[5], f[6])
    end

    # Pick the candidate - frame type with the highest point number
    highest = duplicate = 0
    candidate = Frame
    was_higher = false
    candidates.each do |k, v|
      if v > highest
        highest = v
        candidate = k
        was_higher = true
      elsif v == highest
        duplicate = v
      end
    end

    # Check if there are multiple highest rated types
    if was_higher && highest == duplicate
      raise "More than one highest rated frame type"
    end

    # Create the frame using correct class and return it
    candidate.create(f)
  end
end

class Frame
  attr_reader :direction, :timestamp
  attr_reader :enet_src, :enet_dst, :protocol
  #protected   :enet_src, :enet_dst, :protocol

  def initialize(enet_src, enet_dst, protocol)
    @enet_src = enet_src.mac
    @enet_dst = enet_dst.mac
    @protocol = protocol.to_s
    @timestamp = Time.at(0)
    @direction = nil
    @local = nil # If true then frame is sent by etherbat
  end

  def self.create(f, subclass_params = [])
    # Get basic frame parameters
    if f.length >= 6
      params = [Host.new_mac(f[2]), Host.new_mac(f[4])]
      # If we are creating subclass then protocol is not specified
      params << f[5] if self == Frame
    else
      raise "Frame too short: #{f.join(' ')}"
    end

    # Use all params to create new frame of correct class
    params.concat subclass_params

    o = self.new(*params)
    # Set timestamp and direction
    # f[0] and f[1] exists - previous code would raise exception if not
    tv_sec, tv_usec = f[1].split(".")
    o.timestamp = Time.at(tv_sec.to_i, tv_usec.to_i)
    debug "Readline bug, line too short: '#{f[0]}'" if f[0].length != 2
    o.direction = if f[0] =~ /i$/
                    :in
                  elsif f[0] =~ /o$/
                    :out
                  else
                    # There should be no other directions nor
                    # local tag in sniffed frames.
                    raise "Invalid frame direction: #{f[0]}"
                  end
    return o # Return newly created object
  end

  # Compares only data contained in physical frame -- other attributes
  # such as timestamp, direction or local are not compared.
  def ==(f)
    # Compare only when classes match
    if self.class == f.class
      @enet_src == f.enet_src && \
      @enet_dst == f.enet_dst && \
      @protocol == f.protocol
    else
      false
    end
  end

  def direction=(direction)
    if @direction.nil?
      if (direction != :in) && (direction != :out)
        raise ArgumentError, "Unknown direction"
      else
        @direction = direction
      end
    else
      raise "Direction could be set only once"
    end
  end

  def timestamp=(ts)
    if Time.at(0) == @timestamp
      if ts.class != Time
        raise ArgumentError, "Invalid timestamp"
      else
        @timestamp = ts
      end
    else
      raise "Timestamp could be set only once"
    end
  end

  def set_local
    if @local.nil?
      @local = true
    else
      raise "Local flag could be set only once"
    end
  end

  def is_local?
    if @local == true
      true
    else # false or nil (not set)
      false
    end
  end

  def to_s
    dir_sign = if @direction == :in
                 "i"
               elsif @direction == :out
                 (is_local? ? "L" : "o" )
               else
                 "?"
               end
    # '\' sign at the end of lines is very important here
    "#{dir_sign}#{dir_sign} #{@timestamp.tv_sec}." \
    + sprintf("%.6d ", @timestamp.tv_usec) + \
    "#@enet_src > #@enet_dst #@protocol"
  end
end

class Arp < Frame
  attr_reader :arp_smac, :arp_sip, :arp_tmac, :arp_tip
  #protected   :arp_smac, :arp_sip, :arp_tmac, :arp_tip

  def initialize(enet_src, enet_dst,
                 arp_smac, arp_sip, arp_tmac, arp_tip,
                 arp_op)
    super(enet_src, enet_dst, "ARP")
    @arp_smac = arp_smac.mac
    @arp_sip = arp_sip.ip
    @arp_tmac = arp_tmac.mac
    @arp_tip = arp_tip.ip
    @arp_op = arp_op
  end

  def self.likeness(type, subtype = nil)
    (type == "ARP" ? 1 : 0)
  end

  def self.create(f, subclass_params = [])
    # Get ARP parameters
    if f[5] == "ARP" # If f[5] doesn't exist it is nil
      if f.length >= 11
        params = [Host.new_mac(f[7]), Host.new_ip(f[8]),
                  Host.new_mac(f[9]), Host.new_ip(f[10])]
        # Use arp_op if generic Arp class wanted, otherwise
        # subclasses specify additional parameters in subclass_params.
        params << f[6] if self == Arp
      else
        raise "ARP packet too short"
      end
    else
      raise "Trying to create Arp class from non-arp frame"
    end

    super(f, params.concat(subclass_params))
  end

  # Compares only data contained in physical arp packet -- other attributes
  # such as timestamp, direction or local are not compared.
  def ==(f)
    # Compare only when classes match
    if self.class == f.class
      @arp_smac == f.arp_smac && \
      @enet_dst == f.enet_dst && \
      @protocol == f.protocol && super(f)
    else
      false
    end
  end

  def to_s
    super + " #@arp_op #@arp_smac #@arp_sip #@arp_tmac #@arp_tip"
  end

  # Need to be invoked after class definition
  FrameFactory.register(self)
end

class ArpReq < Arp
  def initialize(enet_src, enet_dst, arp_smac, arp_sip, arp_tmac, arp_tip)
    super(enet_src, enet_dst,
          arp_smac, arp_sip, arp_tmac, arp_tip, "Request")
  end

  def self.likeness(type, subtype)
    (type == "ARP" && subtype == "Request" ? 2 : 0)
  end

  def self.create(f, subclass_params = [])
    # Get ARP operation
    if f[6] == "Request" # If f[6] doesn't exist it is nil
      if f.length >= 7
        # Do nothing - everything will be done by superclasses
      else
        raise "ARP Request packet too short"
      end
    else
      raise "Trying to create Arp Request class from non-request frame"
    end

    super(f, subclass_params)
  end

  # Need to be invoked after class definition
  FrameFactory.register(self)
end

class ArpReply < Arp
  def initialize(enet_src, enet_dst, arp_smac, arp_sip, arp_tmac, arp_tip)
    super(enet_src, enet_dst,
          arp_smac, arp_sip, arp_tmac, arp_tip, "Reply")
  end

  def self.likeness(type, subtype)
    (type == "ARP" && subtype == "Reply" ? 2 : 0)
  end

  def self.create(f, subclass_params = [])
    # Get ARP operation
    if f[6] == "Reply" # If f[6] doesn't exist it is nil
      if f.length >= 7
        # Do nothing - everything will be done by superclasses
      else
        raise "ARP Reply packet too short"
      end
    else
      raise "Trying to create Arp Reply class from non-reply frame"
    end

    super(f, subclass_params)
  end

  # Creates asymetric ArpReply from reply_sender to ArpReq
  # specified as req.
  def self.asym_reply_to(req, reply_sender)
    arp_smac = Host.new_mac(req.arp_smac)
     arp_sip = Host.new_ip(req.arp_sip)

    self.new(reply_sender, arp_smac,
             reply_sender, reply_sender, arp_smac, arp_sip)
  end

  # Same as asym_reply_to but creates symetric ArpReply
  # FIXME: Check how symetric reply looks like in details
  # - is it matched?
  def self.sym_reply_to(req, reply_sender)
    enet_src = Host.new_mac(req.enet_src)
     arp_sip = Host.new_ip(req.arp_sip)

    self.new(reply_sender, enet_src,
             reply_sender, reply_sender, enet_src, arp_sip)
  end

  # Need to be invoked after class definition
  FrameFactory.register(self)
end

class OptParse
  attr_reader :host_1_ip, :host_1_mac, :host_2_ip, :host_2_mac, \
              :host_0_ip, :host_0_mac, :host_b_mac, \
              :debug, :raw, :iface, :repeat_count, :optimistic, \
              :one_way_time, :reply_time
  def initialize(args)
    @iface = "eth0"
    @debug = false
    @host_b_mac = "40:00:00:00:00:eb" # locally administered mac
    @repeat_count = 3
    @raw = false
    @optimistic = false
    @one_way_time = 0.009
    @reply_time = 0.002
    version = "1.0.1"

    opts = OptionParser.new
    opts.banner = "Usage: #{opts.program_name} [options] " + \
    "host_1_ip[,host_1_mac] host_2_ip[,host_2_mac]"
    opts.on("-i interface", String, "default: #@iface") {|val| @iface = val}
    opts.on("-0", "--host-0 ip,mac", String,
            "default: ip and mac of the interface") do |val|
      @host_0_ip, @host_0_mac = val.split(",")
      if ! @host_0_mac
        puts "#{opts.program_name}: invalid host_0 string"
        exit
      end
    end
    opts.on("-b", "--host-b mac", String,
            "default: #@host_b_mac") do |val|
      @host_b_mac = val
    end
    opts.on("-r", "--repeat times", Integer,
            "repeat each test when failed, default: " + \
            "x #@repeat_count") do |val|
      @repeat_count = val
    end
    opts.on("--one-way-time seconds", Float,
            "time frame traverses net, default: " + \
            "#@one_way_time") do |val|
      @one_way_time = val
    end
    opts.on("--reply-time seconds", Float,
            "time host reply to ARP, default: " + \
            "#@reply_time") do |val|
      @reply_time = val
    end

    opts.on("-o", "--optimistic",
            "make optimistic assumptions in analysis") do
      @optimistic = true
    end
    opts.on("-d", "--debug", "display debug information") {@debug = true}
    opts.on("--raw", "display raw results, for debug") {@raw = true}
    opts.on("--version", "show version") do
      puts "#{opts.program_name} #{version}"
      exit
    end
    opts.on_tail("-h", "--help", "show this message") do
      puts opts
      exit
    end
    rest = opts.parse(args)
    if rest.length == 2
      @host_1_ip, @host_1_mac = rest[0].split(",")
      @host_2_ip, @host_2_mac = rest[1].split(",")
    else
      puts "#{opts.program_name}: too few arguments, use -h to see help"
      exit
    end
  end
end

# ------------------------
# Main program starts here
# ------------------------

options = OptParse.new(ARGV)
# TODO: make it more elegant
EB_DEBUG = options.debug
OPTIMISTIC = options.optimistic

# Time constants
OWT = options.one_way_time # One way time
REPT = options.reply_time # Reply time
RTT = OWT * 2 + REPT # Round trip time


if options.host_0_ip && options.host_0_mac
  host_0 = Host.new(options.host_0_ip, options.host_0_mac)
else
  host_0 = Host.create_self(options.iface)
end
host_0.name = "0"

# Subprocesses need to be started before mac resolving
sniffer = Sniffer.create(options.iface)
injector = Injector.create(options.iface)

host_1 = Host.new_ip(options.host_1_ip)
if options.host_1_mac
  host_1.set_mac(options.host_1_mac)
else
  ret = MacResolver.new(host_0).resolve!(host_1)
  if ! ret
    fatal "Host 1 mac could not be resolved"
    exit
  end
end
host_1.name = "1"

host_2 = Host.new_ip(options.host_2_ip)
if options.host_2_mac
  host_2.set_mac(options.host_2_mac)
else
  ret = MacResolver.new(host_0).resolve!(host_2)
  if ! ret
    fatal "Host 2 mac could not be resolved"
    exit
  end
end
host_2.name = "2"

host_b = Host.new_mac(options.host_b_mac)
PAUSE = Host.new_mac("01:80:c2:00:00:01")

if host_1.ip == host_2.ip
  fatal "Host 1 ip == host 2 ip, tests not possible"
  exit
end

if host_1.ip == host_0.ip
  warning "Host 1 ip == host 0 ip, assuming you know what you are doing"
elsif host_2.ip == host_0.ip
  warning "Host 2 ip == host 0 ip, assuming you know what you are doing"
end

if host_1.mac == host_2.mac
  fatal "Host 1 mac = host 2 mac, tests not possible"
  exit
elsif host_1.mac == host_0.mac
  fatal "Host 1 mac = host 0 mac, tests not possible"
  exit
elsif host_2.mac == host_0.mac
  fatal "Host 2 mac = host 0 mac, tests not possible"
  exit
end

if host_b.mac == host_0.mac
  warning "Host b mac = host 0 mac, assuming you know what you are doing"
elsif host_b.mac == host_1.mac
  fatal "Host 1 mac == host b mac, tests not possible"
  exit
elsif host_b.mac == host_2.mac
  fatal "Host 2 mac == host b mac, tests not possible"
  exit
end

begin
  puts "0: #{host_0}"
  puts "1: #{host_1}"
  puts "2: #{host_2}"

  seq = TestSequence.new(host_0, host_1, host_2, host_b)
  if seq.perform(options.repeat_count)
    seq.results.each do |r|
      puts "\n#{r}"
    end
    puts "\nRaw results: #{seq.raw_results}" if options.raw
  end
rescue
  sniffer.close
  injector.close
  raise
end

sniffer.close
injector.close
