# Jettywrapper is a Singleton class, so you can only create one jetty instance at a time.
#require 'logger'
require 'loggable'
require 'singleton'
require 'fileutils'
require 'socket'
require 'timeout'
require 'childprocess'
require 'active_support/core_ext/hash'

Dir[File.expand_path(File.join(File.dirname(__FILE__),"tasks/*.rake"))].each { |ext| load ext } if defined?(Rake)


class Jettywrapper
  
  include Singleton
  include Loggable
  
  attr_accessor :pid          # If Jettywrapper is running, what pid is it running as? 
  attr_accessor :port         # What port should jetty start on? Default is 8888
  attr_accessor :jetty_home   # Where is jetty located? 
  attr_accessor :startup_wait # After jetty starts, how long to wait until starting the tests? 
  attr_accessor :quiet        # Keep quiet about jetty output?
  attr_accessor :solr_home    # Where is solr located? Default is jetty_home/solr
  attr_accessor :base_path    # The root of the application. Used for determining where log files and PID files should go.
  attr_accessor :java_opts    # Options to pass to java (ex. ["-Xmx512mb", "-Xms128mb"])
  
  # configure the singleton with some defaults
  def initialize(params = {})
    if defined?(Rails.root)
      @base_path = Rails.root
    else
      @base_path = "."
    end

    logger.debug 'Initializing jettywrapper'
  end
  
  # Methods inside of the class << self block can be called directly on Jettywrapper, as class methods. 
  # Methods outside the class << self block must be called on Jettywrapper.instance, as instance methods.
  class << self
    
    def load_config
      if defined? Rails 
        config_name =  Rails.env 
        app_root = Rails.root
      else 
        config_name =  ENV['environment']
        app_root = ENV['APP_ROOT']
        app_root ||= '.'
      end
      filename = "#{app_root}/config/jetty.yml"
      begin
        file = YAML.load_file(filename)
      rescue Exception => e
        logger.warn "Didn't find expected jettywrapper config file at #{filename}, using default file instead."
        file ||= YAML.load_file(File.join(File.dirname(__FILE__),"../config/jetty.yml"))
        #raise "Unable to load: #{file}" unless file
      end
      file.with_indifferent_access
    end
    

    # Set the jetty parameters. It accepts a Hash of symbols. 
    # @param [Hash<Symbol>] params
    # @param [Symbol] :jetty_home Required. Where is jetty located? 
    # @param [Symbol] :jetty_port What port should jetty start on? Default is 8888
    # @param [Symbol] :startup_wait After jetty starts, how long to wait before running tests? If you don't let jetty start all the way before running the tests, they'll fail because they can't reach jetty.
    # @param [Symbol] :solr_home Where is solr? Default is jetty_home/solr
    # @param [Symbol] :quiet Keep quiet about jetty output? Default is true. 
    # @param [Symbol] :java_opts A list of options to pass to the jvm 
    def configure(params = {})
      hydra_server = self.instance
      hydra_server.quiet = params[:quiet].nil? ? true : params[:quiet]
      if defined?(Rails.root)
       base_path = Rails.root
      elsif defined?(APP_ROOT)
       base_path = APP_ROOT
      else
       raise "You must set either Rails.root, APP_ROOT or pass :jetty_home as a parameter so I know where jetty is" unless params[:jetty_home]
      end
      hydra_server.jetty_home = params[:jetty_home] || File.expand_path(File.join(base_path, 'jetty'))
      hydra_server.solr_home = params[:solr_home]  || File.join( hydra_server.jetty_home, "solr")
      hydra_server.port = params[:jetty_port] || 8888
      hydra_server.startup_wait = params[:startup_wait] || 5
      hydra_server.java_opts = params[:java_opts] || []
      return hydra_server
    end
   
     
    # Wrap the tests. Startup jetty, yield to the test task, capture any errors, shutdown
    # jetty, and return the error. 
    # @example Using this method in a rake task
    #   require 'jettywrapper'
    #   desc "Spin up jetty and run tests against it"
    #   task :newtest do
    #     jetty_params = { 
    #       :jetty_home => "/path/to/jetty", 
    #       :quiet => false, 
    #       :jetty_port => 8983, 
    #       :startup_wait => 30
    #     }
    #     error = Jettywrapper.wrap(jetty_params) do   
    #       Rake::Task["rake:spec"].invoke 
    #       Rake::Task["rake:cucumber"].invoke 
    #     end 
    #     raise "test failures: #{error}" if error
    #   end
    def wrap(params)
      error = false
      jetty_server = self.configure(params)
      # jetty_server = self.instance
      # jetty_server.quiet = params[:quiet] || true
      # jetty_server.jetty_home = params[:jetty_home]
      # jetty_server.solr_home = params[:solr_home]
      # jetty_server.port = params[:jetty_port] || 8888
      # jetty_server.startup_wait = params[:startup_wait] || 5

      begin
        # puts "starting jetty on #{RUBY_PLATFORM}"
        jetty_server.start
        sleep jetty_server.startup_wait
        yield
      rescue
        error = $!
        puts "*** Error starting hydra-jetty: #{error}"
      ensure
        # puts "stopping jetty server"
        jetty_server.stop
      end

      return error
    end
    
    # Convenience method for configuring and starting jetty with one command
    # @param [Hash] params: The configuration to use for starting jetty
    # @example 
    #    Jettywrapper.start_with_params(:jetty_home => '/path/to/jetty', :jetty_port => '8983')
    def start(params)
       Jettywrapper.configure(params)
       Jettywrapper.instance.start
       return Jettywrapper.instance
    end
    
    # Convenience method for configuring and starting jetty with one command. Note
    # that for stopping, only the :jetty_home value is required (including other values won't 
    # hurt anything, though). 
    # @param [Hash] params: The jetty_home to use for stopping jetty
    # @return [Jettywrapper.instance]
    # @example 
    #    Jettywrapper.stop_with_params(:jetty_home => '/path/to/jetty')
    def stop(params)
       Jettywrapper.configure(params)
       Jettywrapper.instance.stop
       return Jettywrapper.instance
    end
    
    # Determine whether the jetty at the given jetty_home is running
    # @param [Hash] params: :jetty_home is required. Which jetty do you want to check the status of?
    # @return [Boolean]
    # @example
    #    Jettywrapper.is_jetty_running?(:jetty_home => '/path/to/jetty')
    def is_jetty_running?(params)      
      Jettywrapper.configure(params)
      pid = Jettywrapper.instance.pid
      return false unless pid
      true
    end
    
    # Return the pid of the specified jetty, or return nil if it isn't running
    # @param [Hash] params: :jetty_home is required.
    # @return [Fixnum] or [nil]
    # @example
    #    Jettywrapper.pid(:jetty_home => '/path/to/jetty')
    def pid(params)
      Jettywrapper.configure(params)
      pid = Jettywrapper.instance.pid
      return nil unless pid
      pid
    end
    
    # Check to see if the port is open so we can raise an error if we have a conflict
    # @param [Fixnum] port the port to check
    # @return [Boolean]
    # @example
    #  Jettywrapper.is_port_open?(8983)
    def is_port_in_use?(port)
      begin
        Timeout::timeout(1) do
          begin
            s = TCPSocket.new('127.0.0.1', port)
            s.close
            return true
          rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
            return false
          rescue
            return false
          end
        end
      rescue Timeout::Error
      end

      return false
    end
    
    # Check to see if the pid is actually running. This only works on unix. 
    def is_pid_running?(pid)
      begin
        return Process.getpgid(pid) != -1
      rescue Errno::ESRCH
        return false
      end
    end
    
    end #end of class << self
    
        
   # What command is being run to invoke jetty? 
   def jetty_command
     opts = (java_variables + java_opts).join(' ')
     "java #{opts} -jar start.jar"
   end

   def java_variables
     ["-Djetty.port=#{@port}",
      "-Dsolr.solr.home=#{@solr_home}"]
   end

   # Start the jetty server. Check the pid file to see if it is running already, 
   # and stop it if so. After you start jetty, write the PID to a file. 
   # This is the instance start method. It must be called on Jettywrapper.instance
   # You're probably better off using Jettywrapper.start(:jetty_home => "/path/to/jetty")
   # @example
   #    Jettywrapper.configure(params)
   #    Jettywrapper.instance.start
   #    return Jettywrapper.instance
   def start
     logger.debug "Starting jetty with these values: "
     logger.debug "jetty_home: #{@jetty_home}"
     logger.debug "solr_home: #{@solr_home}"
     logger.debug "jetty_command: #{jetty_command}"
     
     # Check to see if we can start.
     # 1. If there is a pid, check to see if it is really running
     # 2. Check to see if anything is blocking the port we want to use     
     if pid
       if Jettywrapper.is_pid_running?(pid)
         raise("Server is already running with PID #{pid}")
       else
         logger.warn "Removing stale PID file at #{pid_path}"
         File.delete(pid_path)
       end
       if Jettywrapper.is_port_in_use?(@jetty_port)
         raise("Port #{self.jetty_port} is already in use.")
       end
     end
     Dir.chdir(@jetty_home) do
       process = build_process
       @pid = process.pid
     end
     FileUtils.makedirs(pid_dir) unless File.directory?(pid_dir)
     begin
       f = File.new(pid_path,  "w")
     rescue Errno::ENOENT, Errno::EACCES
       f = File.new(File.join(@base_path,'tmp',pid_file),"w")
     end
     f.puts "#{@pid}"
     f.close
     logger.debug "Wrote pid file to #{pid_path} with value #{@pid}"
   end
 
   def build_process
     process = ChildProcess.build(jetty_command)
     process.io.inherit!
     process.detach = true
     process.start
   end
   # Instance stop method. Must be called on Jettywrapper.instance
   # You're probably better off using Jettywrapper.stop(:jetty_home => "/path/to/jetty")
   # @example
   #    Jettywrapper.configure(params)
   #    Jettywrapper.instance.stop
   #    return Jettywrapper.instance
   def stop    
     logger.debug "Instance stop method called for pid #{pid}"
     if pid
       process = ChildProcess.new
       process.instance_variable_set(:@pid, pid)
       process.instance_variable_set(:@started, true)
       process.stop 
       begin
         File.delete(pid_path)
       rescue
       end
     end
   end
 

   # The fully qualified path to the pid_file
   def pid_path
     File.join(pid_dir, pid_file)
   end

   # The file where the process ID will be written
   def pid_file
     jetty_home_to_pid_file(@jetty_home)
   end
   
    # Take the @jetty_home value and transform it into a legal filename
    # @return [String] the name of the pid_file
    # @example
    #    /usr/local/jetty1 => _usr_local_jetty1.pid
    def jetty_home_to_pid_file(jetty_home)
      begin
        jetty_home.gsub(/\//,'_') << ".pid"
      rescue
        raise "Couldn't make a pid file for jetty_home value #{jetty_home}"
        raise $!
      end
    end

   # The directory where the pid_file will be written
   def pid_dir
     File.expand_path(File.join(@base_path,'tmp','pids'))
   end
   
   # Check to see if there is a pid file already
   # @return true if the file exists, otherwise false
   def pid_file?
      return true if File.exist?(pid_path)
      false
   end

   # the process id of the currently running jetty instance
   def pid
      File.open( pid_path ) { |f| return f.gets.to_i } if File.exist?(pid_path)
   end
   
end