# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with this
# work for additional information regarding copyright ownership.  The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.

require 'delegate'
require 'drb/drb'

module Buildr

  # This addon allows you start a DRb server hosting a buildfile, so that
  # you can later invoke tasks on it without having to load
  # the complete buildr runtime again.
  #
  # Usage:
  #
  #   buildr -r buildr/drb drb:start
  #
  # Once the server has been started you can invoke tasks using a simple script:
  #
  #   #!/usr/bin/env ruby
  #   require 'rubygems'
  #   require 'buildr/drb'
  #   Buildr::DRbApplication.run
  #
  # Save this script as 'dbuildr', make it executable and use it to invoke tasks.
  #
  #   dbuildr clean compile
  #
  # The dbuildr script will run as the server if there isn't one already running.
  # Subsequent calls to dbuildr will act as the client and invoke the tasks you
  # provide in the server.
  # If the buildfile has been modified it will be reloaded on the BuildrServer.
  #
  # JRuby users can use a nailgun client to invoke tasks as fast as possible
  # without having to incur JVM startup time.
  # See the documentation for buildr/nailgun.
  module DRbApplication

    port = ENV['DRB_PORT'] || 2111
    PORT = port.to_i

    class SavedTask #:nodoc:

      def initialize(original)
        @original = original.clone
        @prerequisites = original.prerequisites.clone if original.respond_to?(:prerequisites)
        @actions = original.actions.clone if original.respond_to?(:actions)
      end

      def name
        @original.name
      end

      def actions
        @actions ||= []
      end

      def prerequisites
        @prerequisites ||= []
      end

      def define!
        @original.class.send(:define_task, @original.name => prerequisites).tap do |task|
          task.comment = @original.comment
          actions.each { |action| task.enhance &action }
        end
      end
    end # SavedTask

    class Snapshot #:nodoc:

      attr_accessor :projects, :tasks, :rules, :layout, :options

      # save the tasks,rules,layout defined by buildr
      def initialize
        @rules = Buildr.application.instance_eval { @rules || [] }.clone
        @options = Buildr.application.options.clone
        @options.rakelib ||= ['tasks']
        @layout = Layout.default.clone
        @projects = Project.instance_eval { @projects || {} }.clone
        @tasks = Buildr.application.tasks.inject({}) do |hash, original|
          unless projects.key? original.name # don't save project definitions
            hash.update original.name => SavedTask.new(original)
          end
          hash
        end
      end

    end # Snapshot

    class << self

      attr_accessor :original, :snapshot

      def run
        begin
          client = connect
        rescue DRb::DRbConnError => e
          run_server!
        else
          run_client(client)
        end
      end

      def client_uri
        "druby://:#{PORT + 1}"
      end

      def remote_run(cfg)
        with_config(cfg) { Buildr.application.remote_run(self) }
      rescue => e
        cfg[:err].puts e.message
        e.backtrace.each { |b| cfg[:err].puts "\tfrom #{b}" }
        raise e
      end

      def save_snapshot(app)
        if app.instance_eval { @rakefile }
          @snapshot = self::Snapshot.new
          app.buildfile_reloaded!
        end
      end

    private

      def server_uri
        "druby://:#{PORT}"
      end

      def connect
        buildr = DRbObject.new(nil, server_uri)
        uri = buildr.client_uri # obtain our uri from the server
        DRb.start_service(uri)
        buildr
      end

      def run_client(client)
        client.remote_run :dir => Dir.pwd, :argv => ARGV,
                          :in  => $stdin, :out => $stdout, :err => $stderr
      end

      def setup
        unless original
          # Create the stdio delegator that can be cached (eg by fileutils)
          delegate_stdio

          # Lazily load buildr the first time it's needed
          require 'buildr'

          # Save the tasks,rules,layout defined by buildr
          # before loading any project
          @original = self::Snapshot.new

          Buildr.application.extend self
          save_snapshot(Buildr.application)
        end
      end

      def run_server
        setup
        DRb.start_service(server_uri, self)
        puts "#{self} waiting on #{server_uri}"
      end

      def run_server!
        setup
        if RUBY_PLATFORM[/java/]
          require 'buildr/nailgun'
          Buildr.application['nailgun:drb'].invoke
        else
          run_server
          DRb.thread.join
        end
      end

      def delegate_stdio
        $stdin  = SimpleDelegator.new($stdin)
        $stdout = SimpleDelegator.new($stdout)
        $stderr = SimpleDelegator.new($stderr)
      end

      def with_config(remote)
        @invoked = true
        set = lambda do |env|
          ARGV.replace env[:argv]
          $stdin.__setobj__(env[:in])
          $stdout.__setobj__(env[:out])
          $stderr.__setobj__(env[:err])
          Buildr.application.instance_variable_set :@original_dir, env[:dir]
        end
        original = {
          :dir => Buildr.application.instance_variable_get(:@original_dir),
          :argv => ARGV,
          :in => $stdin.__getobj__,
          :out => $stdout.__getobj__,
          :err => $stderr.__getobj__
        }
        begin
          set[remote]
          yield
        ensure
          set[original]
        end
      end

    end # class << DRbApplication

    def remote_run(server)
      @options = server.original.options.clone
      init 'Distributed Buildr'
      if @rakefile
        if buildfile_needs_reload?
          reload_buildfile(server, server.original)
        else
          clear_invoked_tasks(server.snapshot || server.original)
        end
      else
        reload_buildfile(server, server.original)
      end
      top_level
    end

    def buildfile_reloaded!
      @last_loaded = buildfile.timestamp if @rakefile
    end

  private

    def buildfile_needs_reload?
      !@last_loaded || @last_loaded < buildfile.timestamp
    end

    def reload_buildfile(server, snapshot)
      clear_for_reload(snapshot)
      load_buildfile
      server.save_snapshot(self)
    end

    def clear_for_reload(snapshot)
      Project.clear
      @tasks = {}
      @rules = snapshot.rules.clone
      snapshot.tasks.each_pair { |name, saved| saved.define! }
      Layout.default = snapshot.layout.clone
    end

    def clear_invoked_tasks(snapshot)
      @rules = snapshot.rules.clone
      (@tasks.keys - snapshot.projects.keys).each do |name|
        if saved = snapshot.tasks[name]
          # reenable this task, restoring its actions/prereqs
          task = @tasks[name]
          task.reenable
          task.prerequisites.replace saved.prerequisites.clone
          task.actions.replace saved.actions.clone
        else
          # tasks generated at runtime, drop it
          @tasks.delete(name)
        end
      end
    end

    task('drb:start') { run_server! } if Buildr.respond_to?(:application)

  end # DRbApplication

end
