#!/usr/bin/env ruby

# Yes, the lexer class is probably overkill, but I had it sitting around from
# another project and code reuse is a good thing =)

# I originally was going to tinker with this until the output ensured that
# each braket is immediately wrapped in a box by iteself, but I think this is
# proably good enough--this works for both of the example erroneous inputs in
# the quiz description--and it's already getting pretty long.

require 'strscan'

class Lexer
  class << self
    def add_rule(regex, token_type = nil, &block)
      @rules ||= []
      block  ||= lambda { |match, token| [token, match] }
      @rules << [ regex, token_type, block ]
    end

    def each_rule
      @rules.each do |rule|
        yield *rule
      end
    end
  end

  def initialize(input)
    @scanner = StringScanner.new(input)
    @tokens  = Array.new()
    @error_context_length = 20
  end

  attr_accessor :error_context_length

  def tokens()
    until @scanner.eos?
      @tokens << find_match()
    end
    @tokens.compact
  end

  def find_match()
    self.class.each_rule do |regex, token, block|
      if (@scanner.scan(regex)) then
        return block.call(@scanner.matched, token)
      end
    end
    raise Exception,
       "Parse error:\n"                      +
       'Can not tokenize input near "'       +
        @scanner.peek(@error_context_length) + '"'
  end
end

class BoxLexer < Lexer
  add_rule(/[\[({]/, :begin_box)
  add_rule(/[\])}]/, :end_box)
  add_rule(/b/i,     :bracket)
end

class Parser
  @@matching_symbol = {
    '(' => ')', ')' => '(',
    '[' => ']', ']' => '[',
    '{' => '}', '}' => '{'
  }

  def initialize input
    @tokens = BoxLexer.new(input).tokens
    @stack  = []
    @corrected_stream = []
  end

  def parse
    while @tokens.size > 0
      @current_token = @tokens.shift
      dispatch_event
    end
    if (@current_token[0] != :end_box)
      notify "!Wrapping all parts in wood '{'"
      @corrected_stream.unshift [:begin_box, '{']
      @corrected_stream << [:end_box, '}']
    end
  end

  attr_reader :corrected_stream

  private

  def notify mesg
    print '  ' * @stack.length
    puts mesg
  end

  def dispatch_event
    case @current_token[0]
      when :begin_box
        begin_event
      when :bracket
        bracket_event
      when :end_box
        end_event
    end
  end

  def begin_event
    value = @current_token[1]
    notify "Found begin box '#{value}'"
    @stack.push value
    @corrected_stream << @current_token
  end

  def bracket_event
    notify "Found a bracket"
    if (@tokens.length < 1) then
      premature_end
    else
      if (@tokens[0][0] != :end_box) then
        notify "! Bracket must be followed by an ending box"
        fix = guess_best_fix
        notify "! Attempting to fix by adding end box: '#{fix}'"
        @tokens.unshift [:end_box, fix]
      end
      @corrected_stream << @current_token
    end
  end

  def end_event
    value = @current_token[1]
    symbol = @@matching_symbol[@stack.pop]
    notify "Found end box '#{value}'"
    if (value != symbol) then
      if symbol then
        notify "! Bad match: Expecting closed '#{symbol}' got '#{value}'"
        notify "! Attempting to fix by adding '#{symbol}'"
        @tokens.unshift @current_token
        @corrected_stream << [:end_box, symbol]
      else
        premature_end
      end
    else
      @corrected_stream << @current_token
    end
  end

  def guess_best_fix
    fix = @@matching_symbol[@stack[-1]]
    if (!fix) then
      notify "! Wow, we really screwed this one up."
      notify "! Use soft packaging ')' because we are cheap ;-)"
      fix = ')'
    end
    fix
  end

  def premature_end
    value = @current_token[1]
    new_sym = @@matching_symbol[value]
    notify "! Premature end of box: found extra '#{value}'"
    notify "! Attempting to fix by wrapping entire payload in '#{new_sym}'"
    @corrected_stream.unshift [:begin_box, new_sym]
    @corrected_stream << [:end_box, value]
  end
end

if (ARGV.length < 1) then
  $stderr.puts "Usage #{__FILE__} box_string"
  exit
end

parser = Parser.new(ARGV[0])
parser.parse
puts parser.corrected_stream.collect{|t| t.last}.join('')