=begin

Copyright 2012, 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 input with code indentation (tab key),
              code highlighting and saving of code snippets.
Usage:        menu Window → Ruby Console+
              Buttons:
              Code Snippets:   Select text in the input or output and click a button to save it.
                  With nothing selected, click a button to paste its text.
              text field "execution context":   Allows to jump into a Ruby Module or Class
                  and execute code within that context (without writing the module/class name every time).
              T:   display time stamps
              #:   use code highlighting and line numbers
              L:   whether to wrap long lines (otherwise there will be a horizontal scrollbar); a bit buggy in IE
              rb/js:   allows to quickly test JavaScript code; however it's only in this WebDialog
              clear:   makes the output field empty; the same when you type "clear".
Version:      1.0.6
Date:         03.10.2012
Notes:        There are four channels for messages to the console:
              * input that the user types is directly added in the WebDialog with
                type: "input"
                (It won't appear in other consoles) TODO: maybe send it with "puts" instead?
              * the result of evaled input is sent back from Ruby to the WebDialog with
                type: "result"
              * if an error occurs on eval or occurs in a script, it is sent from Ruby to the WebDialog with
                type: "error"
              * the $stdout contains all "puts" and "print" messages that are sent with
                type: "stdout"
              They need special handling because "print" is always attached to the previous row
              and "puts" is weirdly achieved by sending a \n as a separate message.


=end

require "sketchup.rb"
dir = File.dirname(__FILE__) # this plugin's folder
require File.join(dir, 'Translate.rb')


module AE; end



class AE::Console < $stdout.class # Sketchup::Console



load File.join(File.dirname(__FILE__), 'Dialog.rb') # TODO: require WebDialogX when stable



  #{#  CLASS VARIABLES
  #
    @@dir = File.dirname(__FILE__) # this plugin's folder

    @@open = false
    @@instance = nil

    @@command  = nil
    @@topmenu  = nil
    @@menuitem = nil

    @@debug = false

  #
  #}#


  #{#  CONSTANTS
  #
    OSX = ( Object::RUBY_PLATFORM =~ /(darwin)/i ? true : false ) unless defined?(Module.nesting[0]::OSX)
    WINE = ( ((!ENV["WINEPREFIX"].nil? || !ENV["WINELOADER"].nil? || File.exists?("Z:/usr/bin/wine")) && !OSX)? true : false ) unless defined?(Module.nesting[0]::WINE)
    WIN = ( !OSX && !WINE ) unless defined?(Module.nesting[0]::WIN)
  #
  #}#


  #{#  CLASS METHODS
  #
    #{ command()
    #
    #  Provide a handle to the command, in case
    #  some other script wants to add it to a toolbar.
    #
    def self.command
      @@command
    end #}


    #{ debug accessors
    #
    def self.debug
      @@debug
    end


    def self.debug?
      @@debug
    end


    def self.debug=(arg)
      @@debug =( arg ? true : false )
    end
    #}


    def self.open(instance)
      @@instance = instance
      if !instance.respond_to?("console_dlg") || instance.console_dlg.nil? # nil if not yet instanced.
        instance.method(:create).call # also opens it.
      else
        # The WebDialog instance exists, just open it.
        instance.open()
      end
    end


    def self.open?
      return @@open
    end


    #{ close()
    #
    #  Cleans up, releases instance, calls GC
    #
    def self.close
      return unless !!defined?(@@instance.console_dlg)
      @@instance.console_dlg.close() if !!defined?(@@instance.console_dlg) && @@instance.console_dlg.visible?
      Kernel.sleep(0.5)
      @@instance.instance_variable_set(:@console_dlg, nil)
      @@instance = nil
      $stdout = $old_stdout
      $stderr = $old_stderr
      GC.start
    end #}
  #
  #}#


  #{#  INSTANCE METHODS
  #
    attr(:console_dlg)


    private


    def initialize
      @console_dlg = nil
      # the context for local variables
      @binding = TOPLEVEL_BINDING
      @last_error_id = $!.object_id
      super
      # Since eval and $stderr (why?) don't catch script errors, we regularly look into the Ruby global $!
      @catch_script_error = Proc.new{
        if @console_dlg && @@open && $!.object_id != @last_error_id
          @console_dlg.call_to("result", {:text=>$!.to_s, :type=>"error", :time=>Time.now.to_f, :backtrace=>$@} )
          @last_error_id = $!.object_id
        end
        UI.start_timer(0.5, false){@catch_script_error.call if @@open}
      }
    end


    protected


    def create()
      # create a webdialog
      @console_dlg = Dialog.new(@@translate["Ruby Console+"],
             false, "AE_Console", 345,400,300,300, true)
      @console_dlg.scope = binding

      ## let SketchUp track size & position

      @console_dlg.set_file( File.join(@@dir, "Console.htm") )
      # @console_dlg.set_background_color( @console_dlg.get_default_dialog_color() ) # does not work on Windows7 ?

      # callbacks
      @console_dlg.add_action_callback("init"){|dlg, param|
        background_color = OSX ? "#E8E8E8!important" : dlg.get_default_dialog_color
        dlg.execute_script('document.getElementsByTagName("body")[0].style.background="' + background_color + '"')
        # translate the dialog
        @@translate.webdialog(dlg)
        # load defaults
        defaults = load_defaults
        dlg.call_to("initialize", defaults)
      }

      # when WebDialog is ready
      @console_dlg.add_action_callback("initialized") { |dlg, param|
        @@open = true
      }

      # for debugging
      @console_dlg.add_action_callback("puts") { |dlg, *args|
        puts(*args)
      }

      # get command from WebDialog for evaluation and send result back
      @console_dlg.add_action_callback("console_eval") { |dlg, command|
        begin
          result = AE::Console.unnested_eval(command, @binding)
          result = result.inspect if !result.is_a?(String)
          dlg.call_to("result", {:text=>result, :type=>"result", :time=>Time.now.to_f} )
        rescue Exception => e
          # Errors of console input would be shown as errors of this file, thus filter them out.
#          includes_console_rb = (@@debug)? false : e.backtrace.inject(false){|bool,trace| trace[/#{__FILE__}/]? true : bool }
#          backtrace = (includes_console_rb)? ['(eval)'] : e.backtrace
          backtrace = e.backtrace.find_all{|b| !b[/#{__FILE__}/] }
          backtrace << '(eval)' if backtrace.empty?
          dlg.call_to("result", {:text=>e.message, :type=>"error", :time=>Time.now.to_f, :backtrace=>backtrace} )
        end
      }
      # cleanup, saving defaults, etc.
      @console_dlg.add_action_callback("save_defaults"){|dlg, defaults|
        puts("AE Console defaults:\n" + defaults.inspect) if @@debug
        success = (defaults!=nil)? save_defaults(defaults) : false
        UI.messagebox(@@translate["%0 (debug)\nDefaults have been saved: '%1'\n",Module.nesting[0],success]) if @@debug
        discard = 6 == UI.messagebox(@@translate["The Console+ data could not be saved. If this happens repeatedly, press 'Yes' to  discard and reset the data."], MB_YESNO) if success == false
        save_defaults({}) if discard
        self.class.close()
      }
      # cleanup, saving defaults, etc.
      @console_dlg.add_action_callback("do_on_close"){|dlg, param| # Depends on JS onunload, possibly not called in some browsers.
        @@open = false
        UI.start_timer(0.3,false) {self.class.close() if !dlg.nil? && dlg.visible?}
      }
      @console_dlg.set_on_close{|dlg, param| # Can be called before save_defaults. Possibly not called on OS X.
        @@open = false;
        UI.start_timer(0.3,false) {self.class.close() if !dlg.nil? && dlg.visible? }
      }
      OSX ? @console_dlg.show_modal : @console_dlg.show
      @catch_script_error.call
      return @console_dlg
    end # create()


    def load_defaults
      defaults = Sketchup.read_default("Plugins_ae", "Console", "{}")
      defaults = eval(defaults) rescue nil
      return defaults
    end


    def save_defaults(defaults)
      defaults = defaults.inspect.inspect[1..-2] # first turn object into String, then escape and remove wrapping quotes
      Sketchup.write_default("Plugins_ae", "Console", defaults)
      # returns the true|false result of Sketchup.write_default()
    end


    public


    def open
      dlg = @console_dlg
      if dlg.visible?
        dlg.bring_to_front
      else
        OSX ? dlg.show_modal() : dlg.show()
      end
    end


    def write(message)
      if @console_dlg && @@open
        if message.to_s==$/ # [/^\n$/]
          # "puts" sends another separate \n afterwards
          @console_dlg.call_to( 'setCreateNewRow' )
        else
          @console_dlg.call_to("result", {:text=>message.to_s, :type=>"stdout", :time=>Time.now.to_f} )
        end
      end
      super(message) # call inherited class write function
    end
  #
  #}#


  #{#  RUN ONCE
  #
  unless file_loaded?("AE::Console:Console")

    @@translate = AE::Translate.new("Console", File.join(@@dir, "lang"))

    @@command = UI::Command.new(@@translate["Ruby Console+"]){
      # in case you want to turn off traces
      $old_stdout = $stdout unless defined?($old_stdout)
      $old_stderr = $stderr unless defined?($old_stderr)
      #
      $stdout = AE::Console.new unless self.open? # stdout goes only into this instance
      $stderr = $stdout # trap error displays as well
      self.open($stdout)
    }

    # Menu
    @@topmenu = UI.menu("Window")
    @@menuitem = @@topmenu.add_item(@@command)

    file_loaded("AE::Console:Console")

  end
  #
  #}#




  # Extension for highlighting SketchUp entities and Point3d
  class HighlightEntity
    @@tool_stack_length = 1
    @@debug = false
    @@instance = nil

    # Change the tool (for being able to draw at the screen)
    # and highlight the entity with given object_id.
    def self.select(id_string)
      e = ObjectSpace._id2ref(id_string.hex/2) rescue nil
      if e.is_a?(Sketchup::Entity) && e.valid?
        Sketchup.active_model.tools.push_tool(@@instance||@@instance = self.new)
        @@instance.select(e)
        @@tool_stack_length += 1
        return true
      else
        puts "Entity #{id_string} is not valid" if @@debug
        return false
      end
    end

    # Change the tool (for being able to draw at the screen)
    # and highlight the given Point.
    def self.point(x, y, z)
      if (p = Geom::Point3d.new(x, y, z) rescue nil)
        Sketchup.active_model.tools.push_tool(@@instance||@@instance = self.new)
        @@instance.select(p)
        @@tool_stack_length += 1
        return true
      else
        return false
      end
    end

    # Change the tool back.
    def self.deselect
      if @@tool_stack_length > 1
        # Be careful, this crashes SketchUp when removing the only tool on the stack.
        Sketchup.active_model.tools.pop_tool
        @@tool_stack_length -= 1
      end
    end

    # Instance methods:

    def initialize
      @entity = nil
      @t = []
    end

    def select(entity)
      @entity = entity
      # find all occurences of the entity (in instances) and collect their transformations
      isCGI = (@entity.is_a?(Sketchup::ComponentInstance) || @entity.is_a?(Sketchup::Group) || @entity.is_a?(Sketchup::Image))
      # paths of the nesting of all instances of @entity => their transformation
      e_paths = {[@entity] => (isCGI)? @entity.transformation : Geom::Transformation.new}
      if @entity.is_a?(Sketchup::Drawingelement)
        until (n=e_paths.keys.find_all{|e| e.first.parent.class != Sketchup::Model}).empty?
          n.each{|a|
            e = a.first
            instances = (e.is_a? Sketchup::ComponentDefinition)? e.instances : e.parent.instances
            instances.each{|i|
              e_paths[ [i].concat(a) ] = i.transformation * e_paths[a]
            }
            e_paths.delete(a)
          }
        end
      end
      @t = e_paths.values
      Sketchup.active_model.active_view.invalidate
      rescue self.class.deselect
    end

    def deactivate(view)
      view.invalidate
    end

    # This is because we want to draw onto the screen only when the cursor hovers the WebDialog. [optional]
    def onMouseMove(flags, x, y, view)
      self.class.deselect
    end

    def draw(view)
      # draw settings
      c = Sketchup.active_model.rendering_options["SectionActiveColor"]
      ct = Sketchup::Color.new(c)
      ct.alpha = 0.25
      view.drawing_color = c
      view.line_width = 5
      # types of entities
      if @entity.is_a?(Geom::Point3d) || @entity.is_a?(Sketchup::Vertex)
        @entity = @entity.position if @entity.is_a?(Sketchup::Vertex)
        view.line_width = 3
        view.draw_points(@entity.transform!(@t[0]), 8, 4, c)
        # TODO: necessary to draw off-screen because of SketchUp bug? (draw_points kills next drawing operation)
        # SketchUp bug: draw_points kills the following drawing operation
        # workaround: this unnecessary offscreen line won't be missed
        view.draw2d( GL_LINES, [-1,0,0], [-1,-1,0])
      elsif @entity.is_a?(Sketchup::Edge) || @entity.is_a?(Sketchup::Curve) || @entity.is_a?(Sketchup::ArcCurve)
        @t.each{|t|
          view.draw(GL_LINE_STRIP, @entity.vertices.collect{|v| v.position.transform!(t)})
        }
      elsif @entity.is_a?(Sketchup::Face)
        @t.each{|t|
          view.drawing_color = c
          ps = @entity.outer_loop.vertices.collect{|v| v.position.transform!(t)}
          view.draw(GL_LINE_LOOP, ps)
          view.drawing_color = ct
          view.draw(GL_POLYGON, ps) if Sketchup.version.to_i >= 8 # support of transparent color
        }
      elsif @entity.is_a?(Sketchup::Group) || @entity.is_a?(Sketchup::ComponentInstance) || @entity.is_a?(Sketchup::Image) || @entity.is_a?(Sketchup::ComponentDefinition)
        bounds = @entity.is_a?(Sketchup::Group)? @entity.entities.parent.bounds : @entity.is_a?(Sketchup::ComponentDefinition)? @entity.bounds : @entity.definition.bounds
        @t.each{|t|
          view.drawing_color = c
          ps = (0..7).collect{|i| bounds.corner(i).transform!(t)}
          # a quad strip around the bounding box
          ps1 = [ps[0],ps[1], ps[2],ps[3], ps[6],ps[7], ps[4],ps[5], ps[0],ps[1]]
          # two quads not covered by the quad strip
          ps2 = [ps[0],ps[2], ps[6],ps[4], ps[1],ps[3], ps[7],ps[5]]
          # quad strips ps1, ps2 can be interpreted as lines, but these are missing:
          ps3 = [ps[0],ps[4], ps[1],ps[5], ps[2],ps[6], ps[3],ps[7]]
          # draw lines
          view.draw(GL_LINES, [ps1, ps2, ps3].flatten)
          # draw faces
          if Sketchup.version.to_i >= 8 # support of transparent color
            view.drawing_color = ct
            view.draw(GL_QUAD_STRIP, ps1)
            view.draw(GL_QUADS, ps2)
          end
        }
      elsif @entity.is_a?(Sketchup::Drawingelement) && !(@entity.is_a?(Sketchup::Text) && !@entity.has_leader?)
        cp = @entity.bounds.center
        # diameter; consider a minimum for Drawingelements that have no diameter
        d = [@entity.bounds.diagonal/2.0, view.pixels_to_model(5, cp)].max
        e = view.camera.eye.vector_to(view.camera.target)
        vec = view.camera.up
        vec.length = d
        t_circle = Geom::Transformation.new(cp, e, 10.degrees)
        @t.each{|t|
          circle = [cp+vec]; (1..36).each{|i| circle << circle.last.transform(t_circle).transform(t) }
          # convert to screen space (so that it won't be covered by other geometry)
          circle.collect!{|p| view.screen_coords(p) }
          view.draw2d(GL_LINE_STRIP, circle)
        }
      end
    end
  end



end # class AE::Console



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