#!/usr/bin/env ruby
# coding: utf-8

require 'pathname'
require 'optparse'

#----------------------------------------------------------------------------

def error(line, message)
  STDERR.puts "error: line %d: %s" % [line, message]
  exit 1
end

#----------------------------------------------------------------------------

class String

  @@keywords = "end for next data input# input dim read let goto run if
                restore gosub return rem stop on wait load save verify def
                poke print# print cont list clr cmd sys open close get get# new
                tab( to fn spc( then not step + - * / ^ and or > = < sgn
                int abs usr fre pos sqr rnd log exp cos sin tan atn peek
                len str$ val asc chr$ left$ right$ mid$ ti$ ti st go ~".split(/\s+/)

  @@controls = ["\\$00", "\\$01", "\\$02", "\\$03", "\\$04",
    "\\$05", "\\$06", "\\$07", "\\$08", "\\$09", "\\$0a", "\\$0b",
    "\\$0c", "\\$0d", "\\$0e", "\\$0f", "\\$10", "\\$11", "\\$12",
    "\\$13", "\\$14", "\\$15", "\\$16", "\\$17", "\\$18", "\\$19",
    "\\$1a", "\\$1b", "\\$1c", "\\$1d", "\\$1e", "\\$1f", "\\$20",
    "\\$21", "\\$22", "\\$23", "\\$24", "\\$25", "\\$26", "\\$27",
    "\\$28", "\\$29", "\\$2a", "\\$2b", "\\$2c", "\\$2d", "\\$2e",
    "\\$2f", "\\$30", "\\$31", "\\$32", "\\$33", "\\$34", "\\$35",
    "\\$36", "\\$37", "\\$38", "\\$39", "\\$3a", "\\$3b", "\\$3c",
    "\\$3d", "\\$3e", "\\$3f", "\\$40", "\\$41", "\\$42", "\\$43",
    "\\$44", "\\$45", "\\$46", "\\$47", "\\$48", "\\$49", "\\$4a",
    "\\$4b", "\\$4c", "\\$4d", "\\$4e", "\\$4f", "\\$50", "\\$51",
    "\\$52", "\\$53", "\\$54", "\\$55", "\\$56", "\\$57", "\\$58",
    "\\$59", "\\$5a", "\\$5b", "\\$5c", "\\$5d", "\\$5e", "\\$5f",
    "\\$60", "\\$61", "\\$62", "\\$63", "\\$64", "\\$65", "\\$66",
    "\\$67", "\\$68", "\\$69", "\\$6a", "\\$6b", "\\$6c", "\\$6d",
    "\\$6e", "\\$6f", "\\$70", "\\$71", "\\$72", "\\$73", "\\$74",
    "\\$75", "\\$76", "\\$77", "\\$78", "\\$79", "\\$7a", "\\$7b",
    "\\$7c", "\\$7d", "\\$7e", "\\$7f", "\\$80", "\\$81", "\\$82",
    "\\$83", "\\$84", "\\$85", "\\$86", "\\$87", "\\$88", "\\$89",
    "\\$8a", "\\$8b", "\\$8c", "\\$8d", "\\$8e", "\\$8f", "\\$90",
    "\\$91", "\\$92", "\\$93", "\\$94", "\\$95", "\\$96", "\\$97",
    "\\$98", "\\$99", "\\$9a", "\\$9b", "\\$9c", "\\$9d", "\\$9e",
    "\\$9f", "\\$a0", "\\$a1", "\\$a2", "\\$a3", "\\$a4", "\\$a5",
    "\\$a6", "\\$a7", "\\$a8", "\\$a9", "\\$aa", "\\$ab", "\\$ac",
    "\\$ad", "\\$ae", "\\$af", "\\$b0", "\\$b1", "\\$b2", "\\$b3",
    "\\$b4", "\\$b5", "\\$b6", "\\$b7", "\\$b8", "\\$b9", "\\$ba",
    "\\$bb", "\\$bc", "\\$bd", "\\$be", "\\$bf", "\\$c0", "\\$c1",
    "\\$c2", "\\$c3", "\\$c4", "\\$c5", "\\$c6", "\\$c7", "\\$c8",
    "\\$c9", "\\$ca", "\\$cb", "\\$cc", "\\$cd", "\\$ce", "\\$cf",
    "\\$d0", "\\$d1", "\\$d2", "\\$d3", "\\$d4", "\\$d5", "\\$d6",
    "\\$d7", "\\$d8", "\\$d9", "\\$da", "\\$db", "\\$dc", "\\$dd",
    "\\$de", "\\$df", "\\$e0", "\\$e1", "\\$e2", "\\$e3", "\\$e4",
    "\\$e5", "\\$e6", "\\$e7", "\\$e8", "\\$e9", "\\$ea", "\\$eb",
    "\\$ec", "\\$ed", "\\$ee", "\\$ef", "\\$f0", "\\$f1", "\\$f2",
    "\\$f3", "\\$f4", "\\$f5", "\\$f6", "\\$f7", "\\$f8", "\\$f9",
    "\\$fa", "\\$fb", "\\$fc", "\\$fd", "\\$fe", "\\$ff", "CTRL\\-A",
    "CTRL\\-B", "CTRL\\-C", "CTRL\\-D", "CTRL\\-E", "CTRL\\-F",
    "CTRL\\-G", "CTRL\\-H", "CTRL\\-I", "CTRL\\-J", "CTRL\\-K",
    "CTRL\\-L", "CTRL\\-M", "CTRL\\-N", "CTRL\\-O", "CTRL\\-P",
    "CTRL\\-Q", "CTRL\\-R", "CTRL\\-S", "CTRL\\-T", "CTRL\\-U",
    "CTRL\\-V", "CTRL\\-W", "CTRL\\-X", "CTRL\\-Y", "CTRL\\-Z",
    "CTRL\\-3", "CTRL\\-6", "CTRL\\-7", "SHIFT\\-SPACE", "CBM\\-K",
    "CBM\\-I", "CBM\\-T", "CBM\\-@", "CBM\\-G", "CBM\\-\\+",
    "CBM\\-M", "CBM\\-POUND", "SHIFT\\-POUND", "CBM\\-N", "CBM\\-Q",
    "CBM\\-D", "CBM\\-Z", "CBM\\-S", "CBM\\-P", "CBM\\-A", "CBM\\-E",
    "CBM\\-R", "CBM\\-W", "CBM\\-H", "CBM\\-J", "CBM\\-L", "CBM\\-Y",
    "CBM\\-U", "CBM\\-O", "SHIFT\\-@", "CBM\\-F", "CBM\\-C",
    "CBM\\-X", "CBM\\-V", "CBM\\-B", "SHIFT\\-\\*", "SHIFT\\-A",
    "SHIFT\\-B", "SHIFT\\-C", "SHIFT\\-D", "SHIFT\\-E", "SHIFT\\-F",
    "SHIFT\\-G", "SHIFT\\-H", "SHIFT\\-I", "SHIFT\\-J", "SHIFT\\-K",
    "SHIFT\\-L", "SHIFT\\-M", "SHIFT\\-N", "SHIFT\\-O", "SHIFT\\-P",
    "SHIFT\\-Q", "SHIFT\\-R", "SHIFT\\-S", "SHIFT\\-T", "SHIFT\\-U",
    "SHIFT\\-V", "SHIFT\\-W", "SHIFT\\-X", "SHIFT\\-Y", "SHIFT\\-Z",
    "SHIFT\\-\\+", "CBM\\-\\-", "SHIFT\\-\\-", "SHIFT\\-\\^",
    "CBM\\-\\*", "CBM\\-\\^", "CTRL\\-A", "CTRL\\-B", "stop",
    "CTRL\\-D", "wht", "CTRL\\-F", "CTRL\\-G", "dish", "ensh",
    "\\\\n", "CTRL\\-K", "CTRL\\-L", "\\\\n", "swlc", "CTRL\\-O",
    "CTRL\\-P", "down", "rvon", "home", "del", "CTRL\\-U", "CTRL\\-V",
    "CTRL\\-W", "CTRL\\-X", "CTRL\\-Y", "CTRL\\-Z", "esc", "red",
    "rght", "grn", "blu", "WHT", "up/lo\\ lock\\ on", "up/lo\\ lock\\
    off", "return", "lower\\ case", "DOWN", "RVS\\ ON", "HOME",
    "delete", "esc", "RED", "RIGHT", "GRN", "BLU", "REVERSE\\ ON",
    "white", "down", "reverse\\ on", "home", "red", "right", "green",
    "blue", "WHITE", "RETURN", "DOWN", "RVSON", "HOME", "DEL", "RED",
    "RIGHT", "GREEN", "BLUE", "space", "SPACE", "orng", "f1", "f3",
    "f5", "f7", "f2", "f4", "f6", "f8", "sret", "swuc", "blk", "up",
    "rvof", "clr", "inst", "brn", "lred", "gry1", "gry2", "lgrn",
    "lblu", "gry3", "pur", "left", "yel", "cyn", "orange", "F1", "F3",
    "F5", "F7", "F2", "F4", "F6", "F8", "shift\\ return", "upper\\
    case", "BLK", "UP", "RVS\\ OFF", "CLR", "insert", "BROWN",
    "LT\\.RED", "GRAY1", "GRAY2", "lt\\ green", "LT\\.BLUE", "GRAY3",
    "PUR", "LEFT", "YEL", "cyn", "orange", "f1", "f3", "r5", "f7",
    "f2", "f4", "f6", "f8", "black", "up", "reverse\\ off", "clear",
    "brown", "pink", "dark\\ gray", "gray", "light\\ green", "light\\
    blue", "light\\ gray", "purple", "left", "yellow", "cyan",
    "ORANGE", "F1", "F3", "F5", "F7", "F2", "F4", "F6", "F8", "BLACK",
    "UP", "RVSOFF", "CLR", "INST", "BROWN", "LIG\\.RED", "GRAY\\ 1",
    "GREY\\ 2", "LIG\\.GREEN", "LIG\\.BLUE", "GREY\\ 3", "PURPLE",
    "LEFT", "YELLOW", "CYAN"]

  def self.controls(open, close)
    /#{Regexp.quote(open)}((\d+\s+)?(#{@@controls.join('|')}))#{Regexp.quote(close)}/i
    end

  @@unescaped = self.controls('{', '}')
  @@escaped = self.controls('~', '~')
  
  def label?
    not keyword?
  end

  def keyword?
    @@keywords.include?(self)
  end
  
  def escape!
    gsub! @@unescaped, "~\\1~"
  end

  def unescape!
    gsub! @@escaped, "{\\1}"
  end
end

#----------------------------------------------------------------------------

class IO
  def each_line_with_number(&block)
    number = 0
    each_line do |line|
      yield line, number+=1
    end
  end
end

#----------------------------------------------------------------------------

module Node
  include Enumerable

  attr_writer :parent

  def root?
    parent.nil?
  end

  def leaf?
    children.count == 0
  end

  def parent
    @parent ||= nil
  end

  def children
    @children ||= []
  end

  def addChild(child)
    @children ||= []
    child = [child].flatten

    @children += child
    child.each { |c| c.parent = self }    
  end

  def removeChild(child)
    if children.include? child
      children.delete(child)
      child.parent = nil
      child
    end
  end

  def <<(child)
    addChild(child)
    child
  end

  def next_sibling(clazz=Node)
    node = nil

    after = false    
    return node if root?
    
    parent.each do |sibling|
      if sibling == self
        after = true
        next
      end

      if after && sibling.is_a?(clazz)
        node = sibling
        break
      end
    end
        
    return node
  end

  def next_sibling?(clazz=Node)
    not next_sibling(clazz).nil?
  end
  
  def each(clazz=Node, &block)
    children.each do |child|
      yield child if child.is_a? clazz 
      child.each do |grandchild|
        yield grandchild if grandchild.is_a? clazz
      end
    end
  end
end

#----------------------------------------------------------------------------

class Label
  include Node

  attr_reader :name, :source

  def initialize(name, source=0)
    @name = name
    @source = source
  end

  def path(scope=parent)

    p = parent
    n = name
    
    until p.nil? || p == scope
      n = "%s.%s" % [p.name, n]
      p = p.parent      
    end
    return n
  end
    
  def to_s
    @name
  end
end

#----------------------------------------------------------------------------

class Code < String
  include Node

  def to_s
    gsub(/\s+/, "")
  end
end

#----------------------------------------------------------------------------

class Literal < String
  include Node

  def to_s
    '"%s"' % [self]
  end
end

#----------------------------------------------------------------------------

class Reference
  include Node

  attr_reader :name
  
  def initialize(name)
    @name = name
  end
end

#----------------------------------------------------------------------------

class Line
  include Node

  attr_accessor :source, :file
  
#  @@number = -1
  @@number = 0

  attr_reader :number, :text
  
  def initialize(text, source=0, file="stdin") #FIXME: add source file?
    @text = text
    @file = file
    @source = source
    parse
    cleanup
  end

  def parent=(node)
    super(node)
    @number = @@number+=1
  end
  
  def parse
    @text.unescape!
    
    until @text.empty?

      #if (match = /^["](?<string>[^"]+)["]/.match(@text))
        if (match = /^["](?<string>[^"]*)["]/.match(@text))         #bugfix Double Qutes
        self << Literal.new(match["string"])
        @text = match.post_match.strip
        
      #elsif (match = /^(?<code>then\s*(go(sub|to))|then|go(sub|to))\s*(?<labels>[\w\s,.]+)+/
        elsif (match = /^(?<code>go(sub|to))\s*(?<labels>[\w\s,.]+)+/       #bugfix IF... THEN
                     .match(@text))

        self << Code.new(match["code"])
        labels = match["labels"].split(/\s*,/)

        labels.each do |l|
          l.strip!
          if l.keyword?            
            self << Code.new(l)
            next
          end
          self << Reference.new(l)
          self << Code.new(",") unless l == labels.last
        end
        @text = match.post_match.strip

      #elsif (match = /^(?<separator>:+)/.match(@text)) #original
      elsif (match = /^(?<separator>:)/.match(@text))   #bugfix problem with ::
        self << Code.new(":")
        @text = match.post_match.strip
        
      elsif (match = /^(?<code>[^":]+?(?!then|go(to|sub)))/.match(@text))
        break if match["code"] =~ /\s*rem\s*/

        self << Code.new(match["code"])
        @text = match.post_match.strip
      end
    end
  end

  def cleanup
    last = children.last
    return unless last.is_a? Code

    children.pop if last == ":"
  end
  
  def to_s
    s = "%d " % [number]
    
    children.each do |child|
      if child.is_a? Reference
        begin
          s += parent.resolve(child).to_s
        rescue => e
          error(number, "could not resolve label '#{child.name}'")
        end
      else
        s += child.to_s
      end
    end
    return s
  end
end

#----------------------------------------------------------------------------

class Scope 
  include Node

  attr_reader :name
  
  def initialize(name=nil)
    @name = name.to_s
  end

  def defined?(label)
    children.each do |child|
      next unless child.is_a? Label
      return true if child.name == label.name
    end
    false
  end

  def path
    n = name
    p = parent
    
    until(p.root?)
      n = "%s.%s" % [p.name, n]
      p = p.parent
    end
    
    return n
  end

  def resolve(reference)

    children.each do |label|
      next unless label.is_a? Label
      
      if label.name == reference.name || label.path(parent) == reference.name

        if label.next_sibling?(Line)
          return label.next_sibling(Line).number
        else
          error(label.source,
                "referenced label '%s' not followed by basic code" %
                [label])
        end
      end
    end

    each(Scope) do |scope|
      scope.each(Label) do |label|
        if label.path(self) == reference.name || label.path(parent) == reference.name
          
          if label.next_sibling?(Line)
            return label.next_sibling(Line).number
          else
            error(label.source,
                  "referenced label '%s' not followed by basic code" %
                  [label])
          end
        end
      end
    end

    return parent.resolve(reference)          
  end

  def to_s
    return "global scope" if root?
    return "scope '#{path}'"
  end
end

class Parser

  def initialize(io=STDIN, file="stdin", scope=Scope.new)
    @io = io
    @file = file
    @scope = scope
  end

  def include(type, path, source)
    pathname = Pathname.new(path)

    if not pathname.exist?
      error(source, "include: no such file or directory: '%s'" % [pathname])
    end
    
    if type == "source"
      include_source(pathname)

    elsif type == "data"
      include_data(pathname)

    else
      error(source, "include: unknown type: '%s'" % [type])
    end
  end
  
  def include_source(pathname)
    Parser.new(pathname.open('r'), pathname, @scope).parse
  end

  def include_data(pathname)
    input = pathname.open('rb')
    i = 0
    line = "data "

    input.each_byte do |byte|
      line += "%d," % [byte]

      if i > 0 && i%16 == 0
        @scope << Line.new(line.chop) unless line.empty?
        line = "data "
      end
      
      i+=1
    end
    @scope << Line.new(line.chop) unless line == "data "
    input.close
  end    

  def parse
    @io.each_line_with_number do |line, source|

      line.chomp!
      line.strip!
      line.escape!
      
      next if line.empty?
      next if line =~ /^rem/
      next if line =~ /^;/
      
      if line =~ /^!include\s+(.+?)\s+"(.+?)"/
        match = $&
        include($1, $2, source)          
        line.sub!(match, '')
        redo

      elsif line =~ /^([a-zA-Z_]\w+):/ && $1.label?
        label = Label.new($1, source)

        if @scope.defined? label
          error(source, "label '%s' already defined in %s" % [label, @scope]);
        end

        @scope << label
        line.sub!(/^([a-zA-Z_]\w+):/, '')
        redo
        
      elsif line =~ /^{/
        @scope = @scope << Scope.new(@scope.children.last)
        line.sub!(/^{/, '')
        redo

      elsif line =~ /^([^}]+)/
        @scope << Line.new($1, source, @file)
        line.sub!($1, '')
        redo
        
      elsif line =~ /}$/
        @scope = @scope.parent
        line.sub!(/}$/, '')
        redo        
      end      
    end

    @io.close
    @scope
  end
end

class Preprocessor
  def initialize(input=STDIN, output=STDOUT)
    @input = input
    @output = output
  end
  
  def process()
    @tree = Parser.new(@input, "stdin", Scope.new("global")).parse
  end

  def lookup(num)
    @tree.each(Line) do |line|
      if line.number == num
        puts "%s, line %s" % [line.file, line.source]
        break
      end
    end
  end
       
  def put()
    @tree.each(Line) { |line| puts line }
  end
end

options = {}
OptionParser.new do |opts|
  opts.banner = "Usage: bpp [options]"

  opts.on("-lLINE", "--line=LINE", "Lookup line") do |l|
    options[:line] = l
  end
end.parse!

pp = Preprocessor.new
pp.process

if options.has_key? :line
  pp.lookup(options[:line].to_i)
else
  pp.put
end


		
