require "socket"
require "thread"

require "spring/env"
require "spring/application_manager"
require "spring/process_title_updater"

# Must be last, as it requires bundler/setup
require "spring/commands"

# readline must be required before we setpgid, otherwise the require may hang,
# if readline has been built against libedit. See issue #70.
require "readline"

module Spring
  class Server
    def self.boot
      new.boot
    end

    attr_reader :env

    def initialize(env = Env.new)
      @env          = env
      @applications = Hash.new { |h, k| h[k] = ApplicationManager.new(self, k) }
      @pidfile      = env.pidfile_path.open('a')
      @mutex        = Mutex.new
    end

    def boot
      write_pidfile
      set_pgid
      ignore_signals
      set_exit_hook
      redirect_output
      set_process_title
      watch_bundle

      server = UNIXServer.open(env.socket_name)
      loop { serve server.accept }
    end

    def serve(client)
      client.puts env.version

      app_client       = client.recv_io
      command          = JSON.parse(client.read(client.gets.to_i))
      args, client_env = command.values_at('args', 'env')

      if Spring.command?(args.first)
        client.puts
        client.puts @applications[rails_env_for(args, client_env)].run(app_client)
      else
        client.close
      end
    rescue SocketError => e
      raise e unless client.eof?
    end

    def rails_env_for(args, client_env)
      command = Spring.command(args.first)

      if command.respond_to?(:env)
        env = command.env(args.drop(1))
      end

      env || client_env['RAILS_ENV'] || client_env['RACK_ENV'] || 'development'
    end

    # Boot the server into the process group of the current session.
    # This will cause it to be automatically killed once the session
    # ends (i.e. when the user closes their terminal).
    def set_pgid
      Process.setpgid(0, SID.pgid)
    end

    # Ignore SIGINT and SIGQUIT otherwise the user typing ^C or ^\ on the command line
    # will kill the server/application.
    def ignore_signals
      IGNORE_SIGNALS.each { |sig| trap(sig,  "IGNORE") }
    end

    def set_exit_hook
      server_pid = Process.pid

      # We don't want this hook to run in any forks of the current process
      at_exit { shutdown if Process.pid == server_pid }
    end

    def shutdown
      @applications.values.each(&:stop)

      [env.socket_path, env.pidfile_path].each do |path|
        path.unlink if path.exist?
      end
    end

    def write_pidfile
      if @pidfile.flock(File::LOCK_EX | File::LOCK_NB)
        @pidfile.truncate(0)
        @pidfile.write("#{Process.pid}\n")
        @pidfile.fsync
      else
        exit 1
      end
    end

    # We can't leave STDOUT, STDERR as they as because then they will
    # never get closed for the lifetime of the server. This means that
    # piping, e.g. "spring rake -T | grep db" won't work correctly
    # because grep will hang while waiting for its stdin to reach EOF.
    #
    # However we do want server output to go to the terminal in case
    # there are exceptions etc, so we just open the current terminal
    # device directly.
    def redirect_output
      # ruby doesn't expose ttyname()
      file = open(STDIN.tty? ? `tty`.chomp : "/dev/null", "a")
      STDOUT.reopen(file)
      STDERR.reopen(file)
    end

    def set_process_title
      ProcessTitleUpdater.run { |distance|
        "spring server | #{env.app_name} | started #{distance} ago"
      }
    end

    def watch_bundle
      @bundle_mtime = env.bundle_mtime
    end

    def application_starting
      @mutex.synchronize { exit if env.bundle_mtime != @bundle_mtime }
    end
  end
end