=begin

Copyright 2012-2014, Andreas Eisenbarth
All Rights Reserved

Permission to use, copy, modify, and distribute this software for
any purpose and without fee is hereby granted, provided that the above
copyright notice appear in all copies.

THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

Name:         Console.rb
Author:       Andreas Eisenbarth
Description:  This is another Ruby Console implemented as WebDialog.
              It features a history that is saved over sessions,
              a multi-line code editor with syntax highlighting, indentation, and
              code folding. It supports opening several independent instances of
              the console.
Usage:        menu Window → Ruby Console+
              Clear: Clears the console output
              Reload: Once you have loaded a script manually ("load"), it will be
                added to this menu. All scripts in this list are automatically
                reloaded on when the files are changed.
              Execution context (text field): Insert a name of a Module, Class or
                a reference to an object to set the console's binding to that object.
                This allows you to execute private methods or access private
                instance variables.
              Select: Allows you to click an Entity in the model to get a reference
                to it in the console.
              Preferences: A menu of settings.
                - display time stamps
                - wrap lines
                - wrap into an undo operation (this has only an effect for entity
                  creation, otherwise it makes sense to turn it off)
                - select a theme for the console (colors)
                - select the font size
              Ctrl+Space: triggers the autocompletion. This plugin uses Ruby's
                reflection methods to relevant and accurate autocompletions from
                the live ObjectSpace (instead from a static file).
Version:      2.1.1
Date:         13.02.2014
=end

require "sketchup.rb"



module AE



class Console



DIR = File.dirname(__FILE__) unless defined?(self::DIR)
require(File.join(DIR, "Translate.rb"))
TRANSLATE = Translate.new("Console", File.join(DIR, "lang")) unless defined?(self::TRANSLATE)
require(File.join(DIR, "Dialog.rb"))
require(File.join(DIR, "Options.rb"))
require(File.join(DIR, "HistoryProvider.rb"))
require(File.join(DIR, "HighlightEntity.rb"))
require(File.join(DIR, "FileObserver.rb"))
require(File.join(DIR, "Introspection.rb"))
require(File.join(DIR, "Autocompleter.rb"))
OSX = ( Object::RUBY_PLATFORM =~ /(darwin)/i ) unless defined?(self::OSX)
WINE = ( File.exists?("C:/Windows/system32/winepath.exe" || File.exists?("Z:/usr/bin/wine")) && !OSX) unless defined?(self::WINE)
WIN = ( ( Object::RUBY_PLATFORM =~ /mswin/i || Object::RUBY_PLATFORM =~ /mingw/i ) && !WINE ) unless defined?(self::WIN)



OLD_STDOUT = $stdout unless defined?(OLD_STDOUT)
OLD_STDERR = $stderr unless defined?(OLD_STDERR)
# Keep track of all instances.
@@instances ||= []
def self.open
  # Replace Sketchup::Console
  unless $stdout.is_a?(self::StdOut) && $stderr.is_a?(self::StdErr)
    $stdout = StdOut.new
    $stderr = StdErr.new
  end
  # Start the observer for script errors.
  @@catch_script_error.call if @@instances.empty?
  # Create a new instance
  Console.new
rescue Exception => e
  $stdout = OLD_STDOUT
  $stderr = OLD_STDERR
  raise
end



def self.close(instance=nil)
  unless instance.is_a?(self)
    instance = @@instances.max{ |a, b| a.id <=> b.id }
  end
  instance.close
  @@instances.delete(instance)
  # Set stdout/stderr back to Sketchup::Console.
  if @@instances.empty?
    $stdout = OLD_STDOUT
    $stderr = OLD_STDERR
  end
end



# Create special sub-classes for $stdout and $stderr.
# $stdout and $stderr are normally subclasses of IO which has more methods (<<, puts)
# than Sketchup::Console. We keep them so minimal with only a single `write` method
# to avoid confusion or clashes.


=begin
class StdOut < Sketchup::Console
  def write(*args)
    Console.print(*args)
    super
  end
end



class StdErr < Sketchup::Console
  def write(*args)
    Console.error(*args)
    super
  end
end
=end

class StdOut
  def write(*args)
    Console.print(*args)
    OLD_STDOUT.write(*args)
  end

  def method_missing(meth, *args)
    OLD_STDOUT.send(meth, *args)
  end
end

class StdErr
  def write(*args)
    Console.error(*args)
    OLD_STDERR.write(*args)
  end

  def method_missing(meth, *args)
    OLD_STDERR.send(meth, *args)
  end
end



# Observe script errors.
# Since eval and $stderr (why?) don't catch script errors, we regularly look into the Ruby global $!
@@last_error_id ||= $!.object_id
@@catch_script_error = Proc.new{
  # Check if $! holds a new error.
  if $!.object_id != @@last_error_id
    # Output the error to the console.
    self.error($!.to_s, {:time => Time.now.to_f, :backtrace => $@})
    @@last_error_id = $!.object_id
  end
  # Call this proc again if there is still a console then.
  UI.start_timer(0.5, false) {
    @@catch_script_error.call unless @@instances.empty?
  }
}



# Send messages to consoles.
# The lock variable allows messages from an instance to be directed back to the same instance.
@@lock = nil



def self.puts(*args)
  if @@lock
    @@lock.puts(*args)
  else
    # TODO: Instead of sending the output to all instances, it could be favorable
    # to send it only to the first instance.
    # @@instances.first.puts(*args) unless @@instances.empty?
    @@instances.each{ |instance|
      instance.puts(*args)
    }
  end
  return nil
end



def self.print(*args)
  if @@lock
    @@lock.print(*args)
  else
    @@instances.each{ |instance|
      instance.print(*args)
    }
  end
  return nil
end



def self.error(*args)
  if @@lock
    @@lock.error(*args)
  else
    @@instances.each{ |instance|
      instance.error(*args)
    }
  end
  return nil
end



# Observe status of modifier keys when WebDialog has focus
@@modifier_keys = {} # {"shift" => false, "ctrl" => false, "alt" => false}
def self.shift?
  return !!@@modifier_keys[:shift] || !!@@modifier_keys["shift"]
end

def self.ctrl?
  return !!@@modifier_keys[:ctrl] || !!@@modifier_keys["ctrl"]
end

def self.alt?
  return !!@@modifier_keys[:alt] || !!@@modifier_keys["alt"]
end



# Observe opened selected files if added to the file observer.
# Thos files can be reloaded on change.
@@file_observer ||= FileObserver.new



# Instance methods



@@options ||= Options.new("Console", {
  :verbose            => false,
  :wrap_lines         => true,
  :show_time          => false,
  :wrap_in_undo       => false,
  :verbose            => $VERBOSE,
  :language           => :ruby, # :ruby, :javascript # Not yet supported.
  :binding            => "global",
  :theme              => "/ace/theme/chrome",
  :reload_scripts   => {},
})



attr_reader :id
def initialize
  # Create the smallest unique ID for this console instance.
  # This way the n-th opened console instance will have the window properties (size, position)
  # and history from the last opened n-th instance.
  @id = 0
  @id += 1 while @@instances.find{ |instance| instance.id == @id }
  @@instances[@id] = self

  # Counter for evaled code (only for display in undo stack).
  @undo_counter = 0

  # ID for every message.
  # Each sender of messages (SketchUp Ruby, each JavaScript environment) has unique message ids.
  # The ids allow to track the succession of messages (which error/result was invoked by which input etc.)
  @message_id = "ruby:#{@id}:0"

  # Create a HistoryProvider to load, record and save the history.
  @history = HistoryProvider.new

  # Set the binding in which code is evaluated
  @binding = TOPLEVEL_BINDING
  @binding_object = nil
  set_binding(@@options[:binding])

  # Register scripts for auto-reloading
  if @@instances.length == 1 # if first console instance
    @@options[:reload_scripts].each{ |relpath, enabled|
      next unless enabled
      fullpath = $LOAD_PATH.map{ |base| File.join(base, relpath) }.find{ |path| File.exists?(path) }.to_s
      @@file_observer.register(fullpath, :changed) { |fullpath|
        begin
          load(fullpath)
        rescue LoadError => e
          break self.error(e)
        end
        self.puts(TRANSLATE["%0 reloaded", relpath])
      }
    }
  end

  # Create the dialog.
  @dlg = Dialog.new({
      :dialog_title => TRANSLATE["Ruby Console+"],
      :scrollable => false,
      :preferences_key => "AE_Console#{@id}",
      :height => 300,
      :width => 400,
      :left => 200,
      :top => 200,
      :resizable => true,
      # Custom parameters of AE::Dialog
      # We won't adjust the dialog to its contents. This parameter preserves the
      # dialog size that SketchUp remembered from the previous session.
      :adjust_to_clientsize => false,
      # For convenience, the file is also a parameter.
      :file => File.join(DIR, "html", "Console.html")
  })

  @dlg.on_show{
    # Translate.
    TRANSLATE.webdialog(@dlg)
    # Initialize UI. Send the plugin options and additional data like this console instance's id.
    init_options = @@options.get_all.merge( {:id => @id} )
    @dlg.call_function("AE.Console.initialize", init_options)
  }

  @dlg.on("get_history"){
    @dlg.return @history
  }

  @dlg.on("eval"){ |msg, command, metadata|
    begin
      @@lock = self
      @undo_counter++
      # Wrap it optionally into an operation.
      # TODO: In an MDI, this works only on the focussed model, but the ruby code
      # could theoretically modify another model.
      Sketchup.active_model.start_operation(TRANSLATE["Ruby Console %0 operation %1", @id, @undo_counter], true) if @@options[:wrap_in_undo]
      # Evaluate the code
      result = AE::Console.unnested_eval(command, @binding)
      Sketchup.active_model.commit_operation if @@options[:wrap_in_undo]
      result = result.inspect unless result.is_a?(String)
      @dlg.call_function("AE.Console.result", result, {:time => Time.now.to_f, :id => @message_id.next!, :source => metadata[:id]} )
    rescue Exception => e
      Sketchup.active_model.abort_operation if @@options[:wrap_in_undo]
      # Errors of console input would be shown as errors of this file, thus filter them out.
      backtrace = e.backtrace.find_all{ |trace| !trace[/#{__FILE__}/] }
      backtrace << '(eval)' if backtrace.empty?
     @dlg.call_function("AE.Console.error", e.message, {:time => Time.now.to_f, :backtrace => backtrace, :id => @message_id.next!, :source => metadata[:id]} )
    else
      @history << command
    ensure
      @@lock = nil
    end
  }

  @dlg.on("reload"){ |msg, scripts|
    # Selected scripts will be reloaded.
    # Lock eventual script errors to this console so they don't appear on other consoles.
    @@lock = self
    errors = []
    scripts.each{ |script|
      begin
        load(script)
      rescue Exception => e
        errors << e
      end
    }
    errors.each{ |e| self.error(e) }
    @@lock = nil
  }

  @dlg.on("update_options"){ |msg, hash|
    @@options.update(hash)
  }

  @dlg.on("set_binding"){ |msg, string|
    @dlg.return set_binding(string)
  }

  @dlg.on("set_verbose"){ |msg, bool_or_nil|
    @@options[:verbose] = $VERBOSE = bool_or_nil if [true, false, nil].include?(bool_or_nil)
  }

  @dlg.on("highlight_entity"){ |msg, id|
    success = HighlightEntity.entity(id.to_i)
    msg.return success
  }

  @dlg.on("modifier_keys"){ |msg, hash|
    @@modifier_keys.merge!(hash)
  }

  @dlg.on("highlight_point"){ |msg, array|
    HighlightEntity.point(*array)
  }

  @dlg.on("highlight_stop"){
    HighlightEntity.stop
  }

  @dlg.on("select_entity"){ |msg, desired_name|
    SelectEntity.select_tool(desired_name, @binding){ |name|
      # Asynchronous webdialog callback.
      msg.return name
    }
  }

  @dlg.on("autocomplete"){ |msg, context, prefix|
    wordhash = Autocompleter.complete(context, prefix, @binding_object)
    msg.return wordhash
  }

  @dlg.on("start_observe_file_changed"){ |msg, relpath|
    fullpath = $LOAD_PATH.map{ |base| File.join(base, relpath) }.find{ |path| File.exists?(path) }.to_s
    # Load it immediately
    begin
      load(fullpath)
    rescue LoadError => e
      break self.error(e)
    end
    @@file_observer.register(fullpath, :changed) { |fullpath|
      begin
        load(fullpath)
      rescue LoadError => e
        break self.error(e)
      end
      self.puts(TRANSLATE["%0 reloaded", relpath])
    }
    # Refresh reload menu list in all console instances.
    @@instances.each{ |instance|
      next if instance == self
      dlg = instance.__send__(:instance_variable_get, :@dlg)
      next unless dlg && dlg.visible?
      dlg.call_function("AE.Console.Extensions.ReloadScripts.update", @@options[:reload_scripts])
    }
  }

  @dlg.on("stop_observe_file"){ |msg, relpath|
    fullpath = $LOAD_PATH.map{ |base| File.join(base, relpath) }.find{ |path| File.exists?(path) }.to_s
    @@file_observer.unregister(fullpath)
    # Refresh reload menu list in all console instances.
    @@instances.each{ |instance|
      next if instance == self
      dlg = instance.__send__(:instance_variable_get, :@dlg)
      next unless dlg && dlg.visible?
      dlg.call_function("AE.Console.Extensions.ReloadScripts.update", @@options[:reload_scripts])
    }
  }

  @dlg.on_close{
    @@options.save
    @history.save
    @history.close
    SelectEntity.deselect_tool
    self.class.close(self)
    @@file_observer.unregister_all if @@instances.empty?
  }

  OSX ? @dlg.show_modal : @dlg.show
end



def show
  if @dlg.visible?
    @dlg.bring_to_front
  else
    OSX ? @dlg.show_modal : @dlg.show
  end
end



def close
  @dlg.close
end



def inspect
  return "#<#{self.class}:0x#{(self.object_id << 1).to_s(16)}>"
end



# This method sends messages over the stdout/puts channel to the webdialog.
# @params [Object] args Objects that can be turned into a string.
def puts(*args)
  return unless @dlg && @dlg.visible?
  args.each{ |arg|
    @dlg.call_function("AE.Console.puts", arg.to_s, {:language => :ruby, :time => Time.now.to_f, :id => @message_id.next!} )
  }
  return nil
end



# This method sends messages over the stdout/print channel to the webdialog.
# @params [Object] args Objects that can be turned into a string.
def print(*args)
  return unless @dlg && @dlg.visible?
  args.each{ |arg|
    @dlg.call_function("AE.Console.print", arg.to_s, {:language => :ruby, :time => Time.now.to_f, :id => @message_id.next!} )
  }
  return nil
end



# This method sends messages over the stdout/puts channel to the webdialog.
# @param [Exception,String] exception an exception object or a string of an error message
# @param [Hash] metadata if the first argument is a string
def error(exception, hash=nil)
  return unless @dlg && @dlg.visible?
  if exception.is_a?(Exception)
    @dlg.call_function("AE.Console.error", exception.message, {:language => :ruby, :time => Time.now.to_f, :backtrace => exception.backtrace, :id => @message_id.next!})
  else
    message = exception.to_s
    metadata = {:language => :ruby, :time => Time.now.to_f, id => @message_id.next!}
    metadata.merge!(hash) if hash.is_a?(Hash)
    @dlg.call_function("AE.Console.error", message, metadata)
  end
  return nil
end



# This methods sets @binding of the object that is referenced by the given string.
# @param [String] string of a reference name
# @returns [String] string of the reference to which the binding was actually set (the same on success, different on failure).
def set_binding(string)
  # Shortcuts
  if string.is_a?(Binding)
    @binding = string
    return string # TODO: Here the return value is not optimal, it should be a string of the reference name.
  elsif string == "global"
    @binding = TOPLEVEL_BINDING
    @binding_object = nil
    @@options[:binding] = string
    return string
  end
  begin
    # Validation:
    # Allow global, class and instance variables, also nested modules or classes ($, @@, @, ::).
    # Do not allow any syntactic characters like braces or operators etc.
    string = string[/(\$|@@?)?[^\!\"\'\`\@\$\%\|\&\/\(\)\[\]\{\}\,\;\?\<\>\=\+\-\*\/\#\~\\]+/]
    # Get the object.
    # object = AE::Console.unnested_eval(string, @binding)
    object = AE::Console::Introspection.get_object_from_string(string, @binding_object)
    # Get the binding of the object.
    @binding = object.__send__(:binding)
    @binding_object = object
  rescue
    @binding = TOPLEVEL_BINDING
    @binding_object = nil
    return @@options[:binding] = "global"
  else
    # If successfull, remember current binding in options.
    @@options[:binding] = string
    return string
  end
end



unless file_loaded?(__FILE__)

  command = UI::Command.new(TRANSLATE["Ruby Console+"]){
    AE::Console.open
  }
  command.small_icon = File.join(DIR, "images", "icon_console_16.png")
  command.large_icon = File.join(DIR, "images", "icon_console_24.png")
  command.tooltip = TRANSLATE["An alternative Ruby Console with multiple lines, code highlighting, entity inspection and many more features."]
  command.status_bar_text = TRANSLATE["Press the enter key to evaluate the input, and use shit-enter for linebreaks."]

  # Menu
  UI.menu("Window").add_item(command)

  file_loaded(__FILE__)
end



end # class Console



end # module AE



# Eval out of any module nesting
def (AE::Console).unnested_eval(*args)
  return eval(*args)
end
