class MerbHandler < Mongrel::HttpHandler
  @@file_only_methods = ["GET","HEAD"]
  
  # take the name of a directory and use that as the doc root or public
  # directory of your site. This is set to the root of your merb app + '/public'
  # by default.
  def initialize(dir, opts = {})
    @files = Mongrel::DirHandler.new(dir,false)
    @guard = Mutex.new
  end
  
  # process incoming http requests and do a number of things
  # 1. check for rails style cached pages. add .html to the
  # url and see if there is a static file in public that matches.
  # serve that file directly without invoking Merb and be done with it.
  # 2. Serve static asset and html files directly from public/ if
  # they exist.
  # 3. If none of the above apply, we take apart the request url
  # and feed it into Merb::RouteMatcher to let it decide which 
  # controller and method will serve the request.
  # 4. after the controller has done its thing, we check for the
  # X-SENDFILE header. if you set this header to the path to a file
  # in your controller then mongrel will serve the file directly
  # and your controller can go on processing other requests.
  def process(request, response)
    
    start = Time.now
    
    if response.socket.closed?
      return
    end
    
    MERB_LOGGER.info("\nRequest: PATH_INFO: #{request.params[Mongrel::Const::PATH_INFO]}  (#{Time.now.strftime("%Y-%m-%d %H:%M:%S")})")
    
    # Rails style page caching. Check the public dir first for
    # .html pages and serve directly. Otherwise fall back to Merb 
    # routing and request dispatching. 
    path_info = request.params[Mongrel::Const::PATH_INFO]
    page_cached = path_info + ".html"
    get_or_head = @@file_only_methods.include? request.params[Mongrel::Const::REQUEST_METHOD]

    if get_or_head and @files.can_serve(path_info)
      # File exists as-is so serve it up
      MERB_LOGGER.info("Serving static file: #{path_info}")
      @files.process(request,response)
    elsif get_or_head and @files.can_serve(page_cached)
      # Possible cached page, serve it up
      MERB_LOGGER.info("Serving static file: #{page_cached}")
      request.params[Mongrel::Const::PATH_INFO] = page_cached
      @files.process(request,response)
    else
      begin
        # This handles parsing the query string and post/file upload
        # params and is outside of the synchronize call so that
        # multiple file uploads can be done at once.
        controller = nil
        controller, action = handle(request)
        MERB_LOGGER.info("Routing to controller: #{controller.class} action: #{action}\nParsing HTTP Input took: #{Time.now - start} seconds")
        
        # We need a mutex here because ActiveRecord code can be run
        # in your controller actions. AR performs much better in single
        # threaded mode so we lock here for the shortest amount of time 
        # possible. Route recognition and mime parsing has already occured
        # at this point because those processes are thread safe. This 
        # gives us the best trade off for multi threaded performance 
        # of thread safe, and a lock around calls to your controller actions.
        @guard.synchronize {
          controller.dispatch(action)
        }
      rescue Exception => e
        response.start(500) do |head,out|
          head["Content-Type"] = "text/html"
          MERB_LOGGER.info(exception(e))
          out << html_exception(e)
        end  
        return
      end   
      
      sendfile, clength = nil
      response.status = controller.status
      
      # check for the X-SENDFILE header from your Merb::Controller
      # and serve the file directly instead of buffering.
      controller.headers.each do |k, v|
        if k =~ /^X-SENDFILE$/i
          sendfile = v
        elsif k =~ /^CONTENT-LENGTH$/i
          clength = v.to_i
        else
          [*v].each do |vi|
            response.header[k] = vi
          end
        end
      end
      
      if sendfile
        MERB_LOGGER.info("X-SENDFILE: #{sendfile}\nComplete Request took: #{Time.now - start} seconds")
        file_status = File.stat(sendfile)
        response.status = 200
        # Set the last modified times as well and etag for all files
        response.header[Mongrel::Const::LAST_MODIFIED] = file_status.mtime.httpdate
        # Calculated the same as apache, not sure how well the works on win32
        response.header[Mongrel::Const::ETAG] = Mongrel::Const::ETAG_FORMAT % [file_status.mtime.to_i, file_status.size, file_status.ino]
        # send a status with out content length
        response.send_status(file_status.size)
        response.send_header
        response.send_file(sendfile)
      elsif controller.body.respond_to? :read
        response.send_status(clength)
        response.send_header
        while chunk = controller.body.read(16384)
          response.write(chunk)
        end
        if controller.body.respond_to? :close
          controller.body.close
        end
      else
        MERB_LOGGER.info("Response status: #{response.status}\nComplete Request took: #{Time.now - start} seconds\n\n")
        # render response from successful controller
        response.send_status((controller.body||='').length)
        response.send_header
        response.write(controller.body)
      end
    end
  end    
    
  # This is where we grab the incoming request PATH_INFO
  # and use that in the merb routematcher to determine
  # which controller and method to run. 
  # returns a 2 element tuple of:
  # [controller, action]
  def handle(request)
    path = request.params[Mongrel::Const::PATH_INFO].sub(/\/+/, '/')
    path = path[0..-2] if (path[-1] == ?/)
    route = Merb::RouteMatcher.new.route_request(path)
    [ instantiate_controller(route[:controller], request.body, request.params, route), 
      route[:action] ]
  end
  
  # take a controller class name string and reload or require
  # the right controller file then CamelCase it and turn it 
  # into a new object passing in the request and response.
  # this is where your Merb::Controller is instantiated.
  def instantiate_controller(controller_name, req, env, params) 
    if !File.exist?(Merb::Server.config[:dist_root]+"/app/controllers/#{controller_name.snake_case}.rb")
      raise Merb::MissingControllerFile
    end
    begin
      controller_name.import
      return Object.const_get( controller_name.camel_case ).new(req, env, params)
    rescue RuntimeError
      warn "Error getting instance of '#{controller_name.camel_case}': #{$!}"
      raise $!
    end
  end
    
  # format exception message for browser display  
  def html_exception(e)
    "<html><body><h2>Merb Error!</h2><p>#{ e.message } - (#{ e.class })\n" << 
    "#{(e.backtrace or []).join('<br />')}</p></body></html>"
  end  
  
  def exception(e)
    "#{ e.message } - (#{ e.class })\n" << 
    "#{(e.backtrace or []).join("\n")}"
  end
    
end