require 'nodedump'
#
# ftable.rb
#

require 'amstd/orderedhash'


class RubySourceCodeParser

  def RubySourceCodeParser.parse_file( fname, table )
    new().parse_file(fname, table)
  end

  def RubySourceCodeParser.parse_files( fnames, table )
    new().parse_files(fnames, table)
  end

  def parse_file( fname, table )
    parse_add fname, table
    resolve table
    mkxref table
    table
  end

  def parse_files( fnames, table )
    fnames.each do |n|
      parse_add n, table
    end
    resolve table
    mkxref table
    table
  end

  private

  def resolve( table )
    table.each_value do |data|
      data.resolve_symbols table
    end
  end

  def mkxref( table )
    table.each_value do |m|
      m.refs.each do |n|
        dest = table[n]
        dest.add_rev_link m if dest
      end
    end
  end

  #
  # parse main
  #

  def parse_add( fname, table )
    File.open(fname) {|f|
        skip_yacc_grammer f if /\.y\z/ === fname
        parse_add_f f, table
    }
  end

  def skip_yacc_grammer( f )
    while line = f.gets
      break if /\A%%/ === line
    end
    while line = f.gets
      break if /\A%%/ === line
    end
  end

  def parse_add_f( f, table )
    while line = f.gets do
      if /\A\#\s*define\s+/ === line
        lineno = f.lineno
        buf = read_macro(line, f)
        next unless /\A#\s*define\s+\w+\(/ === line
        name = macro_name(buf)
        table[name] = MacroData.new(f.path, lineno, name, macro_body(buf))
        next
      end

      next unless /\A\w/ === line
      next if /_\(\(|\ANORETURN|[{;:=]/ === line

      header1 = line
      lineno = f.lineno
      header2 = f.gets
      next if /\A\#\s*end/ === header2
      if /\A\#\s*if/ === header2
        header2 = f.gets
        until /\A\#\s*endif/ === f.gets
          # skip
        end
      end
      next if /\A\s*(?:union|struct)\s+\w+\s*\{/ === line + header2
      name = get_function_name(header2) or
              raise "no function name (#{lineno}): #{(line+header2).inspect}"
      table[name] = FunctionData.new(f.path, lineno,
                                     name, *read_function(f,header1+header2))
    end
  end

  #
  # macro
  #

  def read_macro( head, f )
    buf = line = head.dup
    while /\\\n/ === line
      line = f.gets
      buf << line.sub(/\s*\\\n/, "\n")
    end
    buf
  end

  def macro_name( str )
    str.slice(/\A#\s*define\s+(\w+)/, 1)
  end

  def macro_body( str )
    str.sub(/\A#\s*define\s+\w+\(\s*(?:\w+(?:\s*,\s*\w+)*\s*)?\)\s*/, '')
  end

  #
  # function
  #

  def get_function_name( str )
    str.slice(/\A\w+/)
  end

  def read_function( f, header )
    while line = f.gets do
      break if /\A\{/ === line
      header << line
    end

    body = line
    while line = f.gets do
      if /\A\#\s*define/ === line
        begin
          line = f.gets
        end while /\\\n/ === line
        next
      end
      body << line
      break if /\A\}/ === line
    end

    return header, body
  end

end   # class RubySourceCodeParser


class FunctionTable < OrderedHash

  def FunctionTable.parse( fname )
    RubySourceCodeParser.parse_file(fname, self.new)
  end

  def FunctionTable.parse_files( fnames )
    RubySourceCodeParser.parse_files(fnames, self.new)
  end

  #
  # by scope
  #

  def remove_links_to_external_function
    each_value do |m|
      m.refs.reject! {|name|
          if not key?(name)
            $stderr.puts "removed: #{m.name} -> #{name}" if $DEBUG
            true
          else
            false
          end
      }
    end
    adjust_reverse_ref_to_ordered_ref
  end

  def remove_links_to_extern_function
    each_value do |m|
      m.refs.reject! {|dest| not key?(dest) or self[dest].extern? }
    end
    adjust_reverse_ref_to_ordered_ref
  end

  def adjust_reverse_ref_to_ordered_ref
    each do |name, m|
      m.rev_links.reject! {|m| not m.refs.index name }
    end
  end
  private :adjust_reverse_ref_to_ordered_ref

  #
  # by structure
  #

  def reject_isolated_functions
    reject! do |name, m|
      m.terminal? and m.rev_links.select {|m| key?(m.name) }.empty?
    end
  end

  def reject_terminals
    reject! do |name, m|
      m.terminal?
    end
  end

  def reject_DAGs( level = nil )
    level ||= 1.0/0   # infinity

    oldsize = self.size + 1
    i = 0
    while self.size < oldsize
      break if i == level
      i += 1
      oldsize = self.size
      $stderr.puts "----- turn #{i}" if $DEBUG
      reject_terminals
    end
    $stderr.puts "----- end loop" if $DEBUG
  end

  #### remove 1 level cycle
  # require 'amstd/enum_falseall'
  # m.refs.false_all? {|n| key?(n) and not m.name == n }

  def remove_self_link
    each do |name, m|
      m.refs.delete name
      m.rev_links.delete m
    end
  end

  #
  # by mark
  #

  def mark( name )
    m = @hash[name]
    return unless m
    return if m.marked?
    m.marked = true
  end
    
  def mark_recursive( name )
    m = @hash[name]
    return unless m
    return if m.marked?
    m.marked = true
    m.refs.each do |n|
      mark_recursive n
    end
  end

  def mark_around( name )
    data = @hash[name] or raise ArgumentError, "no such function: #{name}"
$stderr.puts "marked: #{name}"
    data.marked = true
    data.refs.each do |n|
      f = @hash[n] or next
$stderr.puts "marked: #{n}"
      f.marked = true
    end
    data.rev_links.each do |f|
$stderr.puts "marked: #{f.name}"
      f.marked = true
    end
  end

  def reject_unmarked
    reject! {|name, m|
        if not m.marked?
$stderr.puts "rejected: #{name}"
          true
        else
          false
        end
    }
    clear_all_mark
  end

  def clear_all_mark
    each_value do |m|
      m.marked = false
    end
  end

  #
  # generic
  #

  def reject!
    rejected = []
    @list.dup.each do |name|
      if yield(name, @hash[name])
        delete name
        rejected.push name
      end
    end
    @hash.each_value do |data|
      rejected.each do |rej|
        data.refs.delete rej
        data.rev_links.reject! {|m| m.name == rej }
      end
    end

    rejected.each {|n| $stderr.puts "rejected: #{n}" } if $DEBUG
  end

end   # class FunctionTable


class ElementData

  def initialize( file, line, name, body )
    @file = file
    @line = line
    @name = name
    @body = body

    @refs = using_functions(body)
    @tmp_refs = refering_symbols(body)
    @rev_links = []
    @marked = false
  end

  def using_functions( str )
    list = []
    str.scan(/(\w+)\(/) do |name, |
      list.push name
    end
    list
  end
  private :using_functions

  def refering_symbols( str )
    list = []
    str.scan(/(\w+)/) do |name, |
      list.push name
    end
    list.uniq
  end
  private :refering_symbols

  RESWORDS = %w( sizeof )   # reserved words used with paren

  def resolve_symbols( table )
    @tmp_refs.each do |name|
      ent = table[name]
      next unless ent
      if ent.static?
        @refs.push name if ent.file == @file
      else
        @refs.push name
      end
    end
    @refs = (@refs.uniq - RESWORDS).sort
  end

  def inspect
    "#<#{self.class} #{@name}>"
  end

  alias to_s inspect

  attr_reader :file
  attr_reader :line

  attr_reader :name

  attr_reader :header
  attr_reader :body

  attr_accessor :refs
  attr_reader :rev_links

  attr_accessor :marked
  alias marked? marked

  def add_rev_link( m )
    @rev_links.push m
  end

  def name_with_scope
    @name + (static? ? '' : '*')
  end

  def terminal?
    @refs.empty?
  end

end   # class ElementData


class MacroData < ElementData

  def static?
    true
  end

  def extern?
    false
  end

  def scope_type
    'static'
  end

end   # class MacroData


class FunctionData < ElementData

  def initialize( file, line, name, header, body )
    super file, line, name, body
    @header = header
  end

  attr_reader :header

  def static?
    /static / === @header
  end

  def extern?
    not static?
  end

  def scope_type
    static? ? 'static' : 'extern'
  end

end   # class FunctionData
