require 'loggable'
require 'singleton'
require 'fileutils'
require 'shellwords'
require 'socket'
require 'timeout'
require 'childprocess'
require 'active_support/core_ext/hash'
require 'erb'
require 'yaml'

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


# Jettywrapper is a Singleton class, so you can only create one jetty instance at a time.
class Jettywrapper
  
  include Singleton
  include Loggable
  include ActiveSupport::Benchmarkable
  
  
  attr_accessor :jetty_home   # Jetty's home directory 
  attr_accessor :port         # Jetty's port.  Default is 8888.  Note that attribute is named port, but params passed in expect :jetty_port
  attr_accessor :startup_wait # How many seconds to wait for jetty to spin up. Default is 5.
  attr_accessor :quiet        # true (default) to reduce Jetty's output
  attr_accessor :solr_home    # Solr's home directory. 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"])
  attr_accessor :jetty_opts   # Options to pass to jetty (ex. ["etc/my_jetty.xml", "etc/other.xml"] as in http://wiki.eclipse.org/Jetty/Reference/jetty.xml_usage
  
  # configure the singleton with some defaults
  def initialize(params = {})
    self.base_path = self.class.app_root
  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 url=(url)
      @url = url
    end

    def url
      @url ||= defined?(ZIP_URL) ? ZIP_URL : 'https://github.com/projecthydra/hydra-jetty/archive/v5.2.0.zip'
      @url
    end

    def tmp_dir=(dir)
      @tmp_dir = dir
    end

    def tmp_dir
      @tmp_dir ||= 'tmp'
    end

    def zip_file
      File.join tmp_dir, url.split('/').last
    end

    def jetty_dir
      'jetty'
    end

    def download(url = nil)
      self.url = url if url
      logger.info "Downloading jetty at #{self.url} ..."
      FileUtils.mkdir tmp_dir unless File.exists? tmp_dir
      system "curl -L #{self.url} -o #{zip_file}"
      abort "Unable to download jetty from #{self.url}" unless $?.success?
    end
    
    def unzip
      download unless File.exists? zip_file
      logger.info "Unpacking jetty..."
      tmp_save_dir = File.join tmp_dir, 'jetty_generator'
      system "unzip -d #{tmp_save_dir} -qo #{zip_file}"
      abort "Unable to unzip #{zip_file} into tmp_save_dir/" unless $?.success?

      # Remove the old jetty directory if it exists
      system "rm -r #{jetty_dir}" if File.directory?(jetty_dir)

      # Move the expanded zip file into the final destination.
      expanded_dir = expanded_zip_dir(tmp_save_dir)
      system "mv #{expanded_dir} #{jetty_dir}"
      abort "Unable to move #{expanded_dir} into #{jetty_dir}/" unless $?.success?
    end

    def expanded_zip_dir(tmp_save_dir)
      # This old way is more specific, but won't work for blacklight-jetty
      #expanded_dir = Dir[File.join(tmp_save_dir, "hydra-jetty-*")].first        
      Dir[File.join(tmp_save_dir, "*")].first        
    end

    def clean
      system "rm -rf #{jetty_dir}"
      unzip
    end

    def reset_config
      @app_root = nil
    end

    def app_root
      return @app_root if @app_root
      @app_root = Rails.root if defined?(Rails.root)
      @app_root ||= APP_ROOT if defined?(APP_ROOT)
      @app_root ||= '.'
    end
    
    def load_config
      if defined? Rails 
        config_name =  Rails.env 
      else 
        config_name =  ENV['environment']
      end

      jetty_file = "#{app_root}/config/jetty.yml"

      unless File.exists?(jetty_file)
        logger.warn "Didn't find expected jettywrapper config file at #{jetty_file}, using default file instead."
        jetty_file = File.expand_path("../config/jetty.yml", File.dirname(__FILE__))
      end

      begin
        @jetty_erb = ERB.new(IO.read(jetty_file)).result(binding)
      rescue Exception => e
        raise("jetty.yml was found, but could not be parsed with ERB. \n#{$!.inspect}")
      end

      begin
        @jetty_yml = YAML::load(@jetty_erb)
      rescue StandardError => e
        raise("jetty.yml was found, but could not be parsed.\n")
      end

      if @jetty_yml.nil? || !@jetty_yml.is_a?(Hash)
        raise("jetty.yml was found, but was blank or malformed.\n")
      end

      config = @jetty_yml.with_indifferent_access
      config[config_name] || config[:default]
    end
    

    # Set the jetty parameters. It accepts a Hash of symbols. 
    # @param [Hash<Symbol>] params
    #  :jetty_home Required. Jetty's home direcotry
    #  :jetty_port  Jetty's port.  Default is 8888.   Note that attribute is named port, but params passed in expect :jetty_port
    #  :startup_wait How many seconds to wait for jetty to spin up.  Default is 5. If jetty doesn't finish spinning up, tests can fail because they can't reach jetty.
    #  :solr_home Solr's home directory. Default is jetty_home/solr
    #  :quiet Keep True(default) to reduce jetty's output 
    #  :java_opts options to pass to the jvm (ex. ["-Xmx512mb", "-Xms128mb"])
    #  :jetty_opts options to pass to jetty (ex. ["etc/my_jetty.xml", "etc/other.xml"] as in http://wiki.eclipse.org/Jetty/Reference/jetty.xml_usage
    def configure(params = {})
      jetty_server = self.instance
      jetty_server.reset_process!
      jetty_server.quiet = params[:quiet].nil? ? true : params[:quiet]

      jetty_server.jetty_home = params[:jetty_home] || File.expand_path(File.join(app_root, 'jetty'))
      jetty_server.solr_home = params[:solr_home]  || File.join( jetty_server.jetty_home, "solr")
      jetty_server.port = params[:jetty_port] || 8888
      jetty_server.startup_wait = params[:startup_wait] || 5
      jetty_server.java_opts = params[:java_opts] || []
      jetty_server.jetty_opts = params[:jetty_opts] || []
      return jetty_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,
    #       :jetty_opts => "/etc/jetty.xml"
    #     }
    #     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)

      begin
        jetty_server.start
        yield
      rescue
        error = $!
        logger.error "*** Error starting jetty: #{error}"
      ensure
        # puts "stopping jetty server"
        jetty_server.stop
      end

      raise error if error

      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(: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
     ["java", java_variables, java_opts, "-jar", "start.jar", jetty_opts].flatten
   end

   def java_variables
     ["-Djetty.port=#{@port}",
      "-Dsolr.solr.home=#{Shellwords.escape(@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 "jetty_command: #{jetty_command.join(' ')}"
     
     # 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
     end
     if Jettywrapper.is_port_in_use?(self.port)
       raise("Port #{self.port} is already in use.")
     end
     benchmark "Started jetty" do
       Dir.chdir(@jetty_home) do
         process.start
       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 "#{process.pid}"
       f.close
       logger.debug "Wrote pid file to #{pid_path} with value #{process.pid}"
       startup_wait!
     end
   end

   # Wait for the jetty server to start and begin listening for requests
   def startup_wait!
     begin
     Timeout::timeout(startup_wait) do
       sleep 1 until (Jettywrapper.is_port_in_use? self.port)
     end 
     rescue Timeout::Error
       logger.warn "Waited #{startup_wait} seconds for jetty to start, but it is not yet listening on port #{self.port}. Continuing anyway."
     end
   end
 
   def process
     @process ||= begin
        process = ChildProcess.build(*jetty_command)
        if self.quiet
          process.io.stderr = File.open(File.expand_path("jettywrapper.log"), "w+")
          process.io.stdout = process.io.stderr
           logger.warn "Logging jettywrapper stdout to #{File.expand_path(process.io.stderr.path)}"
        else
          process.io.inherit!
        end
        process.detach = true

        process
      end
   end

   def reset_process!
     @process = nil
   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
       if @process
         @process.stop
       else
         Process.kill("KILL", pid) rescue nil
       end

       begin
         File.delete(pid_path)
       rescue
       end
     end
   end
 

   # The fully qualified path to the pid_file
   def pid_path
     #need to memoize this, becasuse the base path could be relative and the cwd can change in the yield block of wrap
     @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