# swank.rb --- swank server for Ruby.
#
# This is my first Ruby program and looks probably rather strange.  Some
# people write Scheme interpreters when learning new languages, I
# write swank backends.  
#
# Only a few things work.  
# 1. Start the server with something like: ruby -r swank -e swank
# 2. Use M-x slime-connect to establish a connection

require "socket"

def swank(port=4005) 
  accept_connections port, false
end

def start_swank(port_file)
  accept_connections false, port_file
end

def accept_connections(port, port_file)
  server = TCPServer.new("localhost", port || 0)
  puts "Listening on #{server.addr.inspect}\n"
  if port_file
    write_port_file server.addr[1], port_file
  end
  socket = begin server.accept ensure server.close end
  begin
    serve socket.to_io
  ensure
    socket.close
  end
end

def write_port_file(port, filename)
  File.open(filename, File::CREAT|File::EXCL|File::WRONLY) do |f|
    f.puts port 
  end
end

def serve(io)
  main_loop(io)
end

def main_loop(io)
  c = Connection.new(io)
  while true
    catch :swank_top_level do
      c.dispatch(read_packet(io))
    end
  end
end

class Connection
  
  def initialize(io)
    @io = io
  end

  def dispatch(event)
    puts "dispatch: %s\n" % event.inspect
    case event[0]
    when :":emacs-rex"
      emacs_rex *event[1..4]
    else raise "Unhandled event: #{event.inspect}"
    end
  end

  def send_to_emacs(obj)
    payload = write_sexp_to_string(obj)
    @io.write("%06x" % payload.length)
    @io.write payload
    @io.flush
  end

  def emacs_rex(form, pkg, thread, id)
    proc = $rpc_entries[form[0]]
    args = form[1..-1];
    begin
      raise "Undefined function: #{form[0]}" unless proc
      value = proc[*args]
    rescue Exception => exc
      begin
        pseudo_debug exc
      ensure
        send_to_emacs [:":return", [:":abort"], id]
      end
    else
      send_to_emacs [:":return", [:":ok", value], id]
    end
  end

  def pseudo_debug(exc)
    level = 1
    send_to_emacs [:":debug", 0, level] + sldb_info(exc, 0, 20)
    begin
      sldb_loop exc
    ensure
      send_to_emacs [:":debug-return", 0, level, :nil]
    end
  end

  def sldb_loop(exc)
    $sldb_context = [self,exc]
    while true
      dispatch(read_packet(@io))
    end
  end

  def sldb_info(exc, start, _end)
    [[exc.to_s,
      "  [%s]" % exc.class.name,
      :nil],
     sldb_restarts(exc),
     sldb_backtrace(exc, start, _end),
     []]
  end

  def sldb_restarts(exc) 
    [["Quit", "SLIME top-level."]]
  end

  def sldb_backtrace(exc, start, _end)
    bt = []
    exc.backtrace[start.._end].each_with_index do |frame, i|
      bt << [i, frame]
    end
    bt
  end

  def frame_src_loc(exc, frame)
    string = exc.backtrace[frame]
    match = /([^:]+):([0-9]+)/.match(string)
    if match
      file,line = match[1..2]
      [:":location", [:":file", file], [:":line", line.to_i], :nil]
    else
      [:":error", "no src-loc for frame: #{string}"]
    end
  end
  
end

$rpc_entries = Hash.new

$rpc_entries[:"swank:connection-info"] = lambda do ||
    [:":pid", $$,
     :":package", [:":name", "ruby", :":prompt", "ruby> "],
     :":lisp-implementation", [:":type", "Ruby",
                               :":name", "ruby",
                               :":version", RUBY_VERSION]]
end

def swank_interactive_eval(string)
  eval(string,TOPLEVEL_BINDING).inspect
end

$rpc_entries[:"swank:interactive-eval"] = \
$rpc_entries[:"swank:interactive-eval-region"] = \
$rpc_entries[:"swank:pprint-eval"] = lambda { |string|
  swank_interactive_eval string
}

$rpc_entries[:"swank:throw-to-toplevel"] = lambda { 
  throw :swank_top_level
}

$rpc_entries[:"swank:backtrace"] = lambda do |from, to|
  conn, exc = $sldb_context
  conn.sldb_backtrace(exc, from, to)
end

$rpc_entries[:"swank:frame-source-location"] = lambda do |frame|
  conn, exc = $sldb_context
  conn.frame_src_loc(exc, frame)
end

#ignored
$rpc_entries[:"swank:buffer-first-change"] = \
$rpc_entries[:"swank:operator-arglist"] = lambda do 
  :nil
end

$rpc_entries[:"swank:simple-completions"] = lambda do |prefix, pkg|
  swank_simple_completions prefix, pkg
end

# def swank_simple_completions(prefix, pkg)

def read_packet(io)
  header = read_chunk(io, 6)
  len = header.hex
  payload = read_chunk(io, len)
  #$deferr.puts payload.inspect
  read_sexp_from_string(payload)
end

def read_chunk(io, len)
  buffer = io.read(len)
  raise "short read" if buffer.length != len
  buffer
end

def write_sexp_to_string(obj)
  string = ""
  write_sexp_to_string_loop obj, string
  string
end

def write_sexp_to_string_loop(obj, string)
  if obj.is_a? String
    string << "\""
    string << obj.gsub(/(["\\])/,'\\\\\1')
    string << "\""
  elsif obj.is_a? Array
    string << "("
    max = obj.length-1
    obj.each_with_index do |e,i|
      write_sexp_to_string_loop e, string
      string << " " unless i == max
    end
    string << ")"
  elsif obj.is_a? Symbol or obj.is_a? Numeric
    string << obj.to_s
  elsif obj == false
    string << "nil"
  elsif obj == true
    string << "t"
  else raise "Can't write: #{obj.inspect}"
  end
end

def read_sexp_from_string(string)
  stream = StringInputStream.new(string)
  reader = LispReader.new(stream)
  reader.read
end

class LispReader
  def initialize(io)
    @io = io
  end
  
  def read(allow_consing_dot=false)
    skip_whitespace
    c = @io.getc
    case c
    when ?( then read_list(true)
    when ?" then read_string
    when ?' then read_quote
    when nil then raise EOFError.new("EOF during read")
    else
      @io.ungetc(c)
      obj = read_number_or_symbol
      if obj == :"." and not allow_consing_dot
        raise "Consing-dot in invalid context"
      end
      obj
    end
  end

  def read_list(head)
    list = []
    loop do
      skip_whitespace
      c = @io.readchar
      if c == ?) 
        break
      else
        @io.ungetc(c)
        obj = read(!head)
        if obj == :"."
          error "Consing-dot not implemented" # would need real conses
        end
        head = false
        list << obj
      end
    end
    list
  end

  def read_string
    string = ""
    loop do 
      c = @io.getc
      case c 
      when ?" 
        break 
      when ?\\ 
        c = @io.getc
        case c
        when ?\\, ?" then string << c
        else raise "Invalid escape char: \\%c" % c
        end
      else
        string << c
      end
    end
    string
  end

  def read_quote
    [:quote, read]
  end

  def read_number_or_symbol
    token = read_token
    if token.empty?
      raise EOFError.new
    elsif /^[0-9]+$/.match(token)
      token.to_i
    elsif /^[0-9]+\.[0-9]+$/.match(token)
      token.to_f
    else
      token.intern
    end
  end

  def read_token
    token = ""
    loop do
      c = @io.getc
      if c.nil? 
        break
      elsif terminating?(c)
        @io.ungetc(c)
        break
      else
        token << c
      end
    end
    token
  end

  def skip_whitespace
    loop do
      c = @io.getc
      case c 
      when ?\s, ?\n, ?\t then next
      when nil then break
      else @io.ungetc(c); break
      end
    end
  end
  
  def terminating?(char) 
    " \n\t()\"'".include?(char)
  end

end


class StringInputStream
  def initialize(string)
    @string = string
    @pos = 0
    @max = string.length
  end

  def pos() @pos end
  
  def getc
    if @pos == @max
      nil
    else
      c = @string[@pos]
      @pos += 1
      c
    end
  end
  
  def readchar
    getc or raise EOFError.new
  end

  def ungetc(c)
    if @pos > 0 && @string[@pos-1] == c 
      @pos -= 1
    else
      raise "Invalid argument: %c [at %d]" % [c, @pos]
    end
  end
  
end