require 'socket'
require 'thread'
require 'http'
require 'pathname'
require 'open3'
require 'stringio'

class Webserver
  include HTTP

  DEFAULT_HEADERS = {
    "Server" => "vontrapp"
  }
  NTHREADS = 10
  DEFAULT_PORT = 80

  def serv_root
    @serv_root || Pathname.new(ENV["HOME"]) + "public_html"
  end
  def serv_root= p
    @serv_root = Pathname.new p
  end
  def fullpath_resource res
    res.sub serv_root.to_s, ""
  end
  def request_fullpath req
    serv_root + req.resource.sub(%r|^/|, "")
  end

  def initialize port=nil
    port ||= DEFAULT_PORT
    @tcp = TCPServer.new port
    @connections = Queue.new

    puts "accepting connections: #{port}"

    @threads = (0...NTHREADS).map do
      Thread.new do
        process
      end
    end

    @accept = Thread.new do
      until @finished
        @connections << @tcp.accept
      end
    end
  end

  def process
    while con = @connections.pop
      serv_req(con)
      con.close
    end
  end

  def mimetype f
    `file -bi #{f.to_s}`.chomp
  end
  def mime_icon type
    type = type[/^[-\/\w]*/]
    "/icons/" + type.sub("/", "-") + ".png"
  end
  def serv_req con
    req = Request.parse con
    con.close_read
    resource = Pathname.new req.resource
    file = self.serv_root + resource.sub(/^./, "")
    r = Response.new
    r << DEFAULT_HEADERS
    if file.directory? # if it's a directory
      r.content_type = "text/html"
      r << dir_html(file)
    elsif file.executable? # cgi
      cgi_script req, con
      return
    elsif file.file?  # check if file exists
      r.content_type = `file -bi #{file.to_s}`.chomp
      r << file.read
    else # not found!
      r.status = 404
      r.message = "Not found"
      r.content_type = "text/html"
      r << not_found(resource)
    end
    r.content_type += "; charset=utf-8" unless r.content_type[/; /]
    r.send con
  end

  def not_found r
    <<html
<html><head><title>Not found</title>
<link rel="shortcut icon" type="application/x-icon"
href="http://von.fugal.net/images/broken.png"/>
</head><body><h1>404: Not found</h1>
<h2>Resource: #{r}</h2>
<image alt="poop" src="http://von.fugal.net/images/poop.gif"/></body></html>
html
  end

  def dir_html file
    r = fullpath_resource file
    html = <<html
<html>
  <head>
    <title>#{file.basename}</title>
    <link rel="shortcut icon" type="application/x-icon"
      href="/icons/dir.png"/>
  </head>
  <body>
    <h1>Directory listing:</h1>
    <h2>#{file.basename}</h2>
    <ul>
html
    for ent in file.entries
      next if ent.to_s == "."
      f = file + ent
      if f.directory?
        type = "directory"
        icon = "/icons/dir.png"
      else
        type = mimetype f
        icon = mime_icon type
      end
      html << <<html
      <li style="list-style-image: url(#{icon})">
        <a alt="#{ent}" href="#{r + ent}">#{ent}</a>
        Type: #{type}
      </li>
html
     end
     html << <<html
    </ul>
  </body>
</html>
html
  end

  def cgi_script request, con
    file = request_fullpath request
    raise ArgumentError, "Must be executable" unless file.executable?

    p = fork do
      Dir.chdir file.dirname
      ENV.clear
      request.each_pair do |k,v|
        ENV["HTTP_" + (k.upcase.gsub /-/, "_")] = v.to_s
      end
      ENV["SCRIPT_FILENAME"] = file.to_s
      ENV["GATEWAY_INTERFACE"] = "CGI/1.1"
      ENV["REQUEST_METHOD"] = request.method
      ENV["QUERY_STRING"] = request.query
      ENV["REQUEST_URI"] = request.uri
      ENV["SCRIPT_NAME"] = request.resource.split("?").first
      ENV["SERVER_PORT"] = @tcp.addr[1].to_s
      ENV["SERVER_SOFTWARE"] = "ruby server"
      ENV["SERVER_NAME"] = "vontrapp"
      ENV["SERVER_PROTOCOL"] = "HTTP/1.0"
      ENV["REMOTE_ADDR"] = con.addr.last
      response = Response.new
      response << DEFAULT_HEADERS
      Open3.popen3 file do |i,o,e|
        i.print request.body
        i.close
        o.each_line do |line|
          line.chomp!
          break if line == ""
          k, v = line.split ": "
          response << {k => v}
          raise "Premature end of script headers!" if o.eof?
        end
        if response.location
          response.status = 302
          response.message = "Found"
        end
        response << o.read
      end
      response.send con
      con.close
    end
  end

  def join
    @threads.map {|t| t.join}
  end

  def finish
    @finished = true
    @tcp.close
    NTHREADS.times do
      @connections << nil
    end
    join
    puts "finished"
  end
end

if __FILE__ == $0
  w = Webserver.new 1982
  Signal.trap("INT") {w.finish}
  w.join
end
