# Purpose: Examine and debug the FreeBASE databus
#
# $Id: script_runner.rb,v 1.11 2004/09/30 21:22:22 ljulliar Exp $
#
# Authors:  Rich Kilmer <rich@infoether.com>
# Contributors:
#
# This file is part of the FreeRIDE project
#
# This application is free software; you can redistribute it and/or
# modify it under the terms of the Ruby license defined in the
# COPYING file.
#
# Copyright (c) 2002 Rich Kilmer All rights reserved.
#

module FreeRIDE
  class ScriptRunner
    extend FreeBASE::StandardPlugin
    include Fox

    def self.start(plugin)
      @plugin = plugin
      @@script_runner = nil
      # Handle icons
      plugin['/system/ui/icons/ScriptRunner'].subscribe do |event, slot|
        if event == :notify_slot_add
          app = plugin['/system/ui/fox/FXApp'].data
          path = "#{plugin.plugin_configuration.full_base_path}/icons/#{slot.name}.png"
          if FileTest.exist?(path)
            slot.data = Fox::FXPNGIcon.new(app, File.open(path, "rb").read)
            slot.data.create
          end
        end
      end
      cmd_mgr = plugin["/system/ui/commands"].manager

      cmd_run = cmd_mgr.add("App/Run/RunScript","&Run") do |cmd_slot|
	@@script_runner = ScriptRunner.new(plugin)
      end
      plugin["/system/ui/keys"].manager.bind("App/Run/RunScript", :F5)

      # Make run command available at start time only if there is an edit 
      # pane opened
      cmd_run.availability = plugin['/system/ui/current'].has_child?('EditPane')
      cmd_run.icon = "/system/ui/icons/ScriptRunner/run"
      plugin["/system/ui/current/ToolBar"].manager.add_command("Run", "App/Run/RunScript")

      cmd_run.manage_availability do |command|
        plugin['/system/ui/current'].subscribe do |event, slot|
          if slot.name=="EditPane"
            case event
            when :notify_slot_link
              command.availability=true
            when :notify_slot_unlink
              command.availability=false
            end
          end
        end
      end

      # now set up the stop icon and menu item
      cmd_stop = cmd_mgr.add("App/Run/StopScript","&Stop") do |cmd_slot|
	@@script_runner.stop(true) if @@script_runner
      end
      plugin["/system/ui/keys"].manager.bind("App/Run/StopScript", :ctrl, :F2)

      cmd_stop.icon = "/system/ui/icons/ScriptRunner/stop"
      plugin["/system/ui/current/ToolBar"].manager.add_command("Run", "App/Run/StopScript")
     

      cmd_clear = cmd_mgr.add("App/Run/ClearOutput","&Clear Output") do |cmd_slot|
        # FIXME: this is a hack to clear the script runner output
        # AND the debugger output. Ideally it should apply only to the
        # currently visible pane but it's not that easy to do.
        plugin["/system/ui/current/OutputPane"].manager.clear("Run")
        ep_slot = plugin["/system/ui/current/EditPane"]
        ep_slot['actions/clear_errorline'].invoke
        
        dbg = plugin["/system/ui/current/Debugger"]
        dbg.manager.clear() if dbg.is_link_slot?
      end
      plugin["/system/ui/keys"].manager.bind("App/Run/ClearOutput", :ctrl, :F5)
      cmd_mgr.command("App/Run/ClearOutput").icon = "/system/ui/icons/ScriptRunner/clear"
      plugin["/system/ui/current/ToolBar"].manager.add_command("Run", "App/Run/ClearOutput")
      
      
      # Insert the inspector in the Tools menu
      runmenu = plugin["/system/ui/components/MenuPane/Run_menu"].manager
      runmenu.add_command("App/Run/RunScript")
      runmenu.add_command("App/Run/StopScript")
      runmenu.add_command("App/Run/ClearOutput")
      plugin.transition(FreeBASE::RUNNING)
    end


    def initialize(plugin)
      @plugin = plugin
      @cmd_mgr = cmd_mgr = plugin["/system/ui/commands"].manager

      dbg_plugin = plugin['/plugins/rubyide_tools_debugger'].manager
      plugin["/system/ui/current/OutputPane"].manager.show
      
      if dbg_plugin.properties['save_before_running']
	@cmd_mgr.command("App/File/SaveAll").invoke
      end
      
      ep_slot = plugin["/system/ui/current/EditPane"]
      ep_slot['actions/clear_errorline'].invoke
      
      #ep_slot['actions/save'].invoke if ep_slot['actions/modified'].invoke
      #unless ep_slot['actions/modified'].invoke
      file = plugin["/system/ui/current/EditPane"].data
      starter_file = File.join("#{@plugin.plugin_configuration.full_base_path}","script_starter.rb")
      ruby_path = dbg_plugin.properties['path_to_ruby']
      ruby_path = 'ruby' if (ruby_path == '' || ruby_path == nil)
      exec_args = dbg_plugin.properties['cmd_line_options']
      exec_dir = dbg_plugin.properties['working_dir']
      exec_dir = File.expand_path(File.dirname(file)) if (exec_dir == '' || exec_dir == nil)
      @exec_dir = exec_dir

      command = "#{ruby_path} -C \"#{exec_dir}\" -r \"#{starter_file}\" \"#{file}\" #{exec_args} 2>&1"

      plugin["/system/ui/current/OutputPane"].manager.append("Run", "<CMD>>ruby #{file}\n")
      
      # run popen on both Linux and Windows. No popen3 as in the
      # debugger because it is then impossible to keep the synchronization
      # between STDOUT and STDERR output
      @inp = @out = IO.popen(command,"w+")

      @inp.print "pid\n"
      @pid = @out.gets.to_i # get remote process ID
      status("Ruby Process Running (PID= #{@pid})")
      @@script_runner = self

      attach_stdout(@out)
      attach_stdin(@inp)

      @inp.print "go\n" # resume remote process

      @previous_trap_handler = trap("SIGINT") do
	puts "Ruby Process Interrupted (PID = #{@pid})"
	stop(true)
      end

    end

    ##
    # stop the running process
    #
    def stop(interrupt = false)
      begin
	Process.kill("SIGKILL", @pid)
      rescue
	# in case the process already died  - do nothing
      end

      detach_stdout(@out)
      detach_stdin(@inp)

      if interrupt
	@plugin["/system/ui/current/OutputPane"].manager.append("Run", "<CMD>>Process Interrupted!!\n")
	status("Ruby Process Interrupted (PID = #{@pid})")
      else
	@plugin["/system/ui/current/OutputPane"].manager.append("Run", "<CMD>>exit\n")
	status("Ruby Process Exited (PID = #{@pid})")
      end

      @pid = nil
      @@script_runner = nil
      trap("SIGINT",@previous_trap_handler)

     end

    ##
    # monitor the script stdout and print any incoming text
    # to the script runner text console
    #
    def attach_stdout(fh)
      @stdout = fh
      getApp().addInput(fh, INPUT_READ|INPUT_EXCEPT) do |sender, sel, ptr|
        case SELTYPE(sel)
        when SEL_IO_READ
          begin
            text = fh.sysread(5000)

	    if text =~ /SS TT OO PP/
	      text.gsub!(/SS TT OO PP/,'')
	      print_stdout(text)
	      self.stop
	    else
	      print_stdout(text)
	    end
            # if there is an error message then point the faulty line in the editpane
            # open the file if not already loaded in one of the Edit panes.
            # If line is nil it removes the line marker, If file is nil do nothing
            if text =~ /^(.*\.rb):(\d+):/
              err_file, line = $1, $2
              err_file = File.expand_path(File.join(@exec_dir,err_file)) unless File.absolute_path?(err_file)

              ep_slot = @cmd_mgr.command("EditPane/FindFile").invoke(err_file)
              ep_slot = @cmd_mgr.command("App/File/Load").invoke(err_file) if ep_slot.nil?
              ep_slot['actions/make_current'].invoke
              ep_slot['actions/show_errorline'].invoke(line)
            end

	    #getApp.forceRefresh
          rescue EOFError
            detach_stdout(fh)
          end
        when SEL_IO_EXCEPT
          puts 'onPipeExcept'
        end
      end
    end
    
    ##
    # attach stdin of script to the renderer
    #
    def attach_stdin(fh)
      @stdin = fh
    end
    
    ##
    # Detach the stdout input from the text console
    #
    def detach_stdout(fh)
      if fh
	begin
	  print_stdout(fh.sysread(100000)); # flush stdout
	rescue EOFError
	  # Do nothing
	end
        getApp().removeInput(fh, INPUT_READ|INPUT_EXCEPT)
        @stdout = nil
      end
    end
    
    ##
    # Detach the stdin from the text console
    #
    def detach_stdin(fh)
      # @stdin should be freed automatically when pipe is closed
      # but just in case
      @stdin = nil
    end

    ##
    # print script stdout to text console
    #
    def print_stdout(text)
      @plugin["/system/ui/current/OutputPane"].manager.append("Run", text)
    end

    ##
    # Return the FOX FXApp global variable
    #
    def getApp
      @plugin['/system/ui/fox/FXApp'].data
    end

    ##
    # Prompt a message in the status bar
    #
    def status(msg)
      @plugin['/system/ui/current/StatusBar/actions/prompt'].invoke(msg)
    end

  end

end
