=begin

### subclass for UI::WebDialog.
To be replaced by the WebDialogX project

Version:      0.1.5
Date:         26.08.2012

The native WebDialog methods to transfer data are somewhat limited:
  Ruby to WebDialog:
    [Ruby] dlg.execute_script()
     * needs escaping of quotes and backslashes (often twice)
     * no error catching for syntaxt/parsing errors! Otherwise Internet Explorer annoys the user with an unfriendly popup.
     * no length limit (?) (tested in IE8/XP: 10000000+ characters)
     * synchronous: the next Ruby method call begins after this has finished.
  WebDialog to Ruby:
    [JS] window.location="skp:actionCallback@argument"
     * needs url encoding; IE doesn't encode single quotes in the url and SketchUp returns nil
     * even encoded backslashes can be lost (especially uneven number of \ )
     * limited to 2083 characters
     * only one single string argument
     * asynchronous: the next JavaScript function is called immediately even if the Ruby code hasn't finished.
    [Ruby] dlg.get_element_value(element_id)
     * no encoding necessary (?)
     * no length limit (?) (tested on IE8/XP: 10000000++ characters)
     * on single string
     * indirect (invoked by Ruby; can't be called from JavaScript onclose event)

This class provides methods that handle all escaping/encoding and work around the message length limit.
They accept any amount of arguments of any data type of JSON:
  String, Fixnum/Integer, Float, Array, Hash/Object, Boolean, nil/null
  (no custom objects or JavaScript functions or SketchUp entities)
Since JavaScript to Ruby is asynchronous, you can make use of the callback functions "success", "error", "complete"
that trigger after the Ruby callback has finished. Thereof success/complete hold as argument the result of the Ruby callback,
and error holds a string of the Ruby error.

  Ruby to WebDialog:
    dlg = AE::Console::Dialog.new
    [Ruby] dlg.call_to( "functionName", argument1, argument2, ... )
      This calls a JavaScript function and returns the result (synchronous).

    [Ruby] dlg.exec_script( "JavaScriptCode" )
      This executes the given string of JavaScript code, suppresses errors if necessary and returns the result (synchronous).

    [Ruby] dlg.set_element_value( element_id, argument )
      Just for completion.

  WebDialog to Ruby:
    [JS] Dialog.callback( "callbackName", argument1, argument2, ... )
     or  Dialog.callback( optionsObject )
      This calls the Ruby code block that has been created with dlg.add_action_callback
         optionsObject = {name: "callbackName", data: {}, success: function, error: function, complete: function, direct: true/false}
         direct: false (or indirect: true)
           means the data is passed via a hidden input element and the Ruby method get_element_value
         direct: true (or indirect: false)
           means the data is encoded, split in chunks < 2083 characters if necessary and passed via an skp url

    [JS] Dialog.callTo( "methodName", argument1, argument2, ... )
     or  Dialog.callTo( optionsObject )  with optionsObject = {name: "methodName", data: argument, ... }
      This calls the given Ruby method and optionally returns the result to a JavaScript callback.

    [JS] Dialog.execScript( "codeString", optionsObject[optional] )
      This executes the given string of Ruby code and optionally returns the result to a JavaScript callback.


TODO:
# Injection of JS Framework does not play well with reloading with F5 (even if only developers do that).
  Possibly detect reloading/closing with window.onclose? Or: if html is saved in temp folder, inject it into the file directly without execute_script?

=end



class AE::Console::Dialog < UI::WebDialog


  @@javascript_file = "Dialog.js"


  attr_accessor :scope


  def initialize(*args)
    super
    @debug = false
    @callback_data = {}
    @message_id_counter = 0
    @scope = TOPLEVEL_BINDING

    # Reset the subclass' method to the superclass' method
    # to allow reloading the file. [for debugging]
    def add_action_callback(name, &block); super; end
    def execute_script(code_string); super; end

    # Callback for long skp urls to get all chunks first.
    add_action_callback("_collect_data"){|dlg,param|
      next if !param
      id = param.slice!(/^\[\d+\]/)[/\d+/].to_i
      @callback_data[id] = "" if @callback_data[id].nil?
      @callback_data[id] += param.gsub(/%q/, "'").gsub(/%b/, "\\").gsub(/%25/, "%")
    }

    # Modify the execute_script method to cleanup its inserted script element
    def execute_script(code_string)
      super(code_string)
      super("try{ Dialog._cleanUpScripts() } catch(e){ if(Dialog.debug == true){" +
        "Dialog.callTo('puts', 'Error in Dialog.execute_script:\\n' + e) } }"
      );
    end

    # DEBUG: Is it possible that redefining add_action_callback fails in SU7 ?
    # Modify the callback method to inject the original arguments
    # either from get_element_value (indirect) or from skp urls (direct)
    def self.add_action_callback(name, &block)
        super(name){|dlg, id|
        # Fallback in case that raw skp url has been used instead Dialog.callback etc.
        # (so id is already a string argument and not an id number)
        if !id[/^\d+$/]
          puts("Warning: Function tried to set window.location='skp:action@argument' directly " +
          "instead of using Dialog.callback('actionName',arguments)")
          return block.call(dlg, id)
        end
        # Get the message id.
        id = id.to_i
        @message_id_counter = id if id > @message_id_counter
        # Get the original arguments. They have been deposited in the @callback_data hash (direct),
        #   if not, they are in the hidden input element (indirect).
        arguments = (!@callback_data[id].nil?)? @callback_data[id] : get_element_value("Dialog.#{id}")
        arguments = from_json(arguments)
        # Call the block
        # and afterwards the javascipt callbacks success/error and complete if those have been defined
        execute_callback = Proc.new{|id, type, data|
          dlg.execute_script("try{ Dialog._callbacks[#{id}].#{type}(#{data}) } catch(e){ if(Dialog.debug == true){" +
            "Dialog.callTo('puts', 'Error when calling Proc of action_callback in Dialog:\\n' + e) } }"
          );
        }
        result_string = "null"
        begin
          result = block.call(dlg, *arguments)
        rescue Exception => e
          execute_callback.call(id, "error", e.inspect.inspect)
        else
          result_string = to_json(result)
          execute_callback.call(id, "success", result_string)
        ensure
          execute_callback.call(id, "complete", result_string)
          dlg.execute_script("Dialog._cleanUp(#{id})")
          @callback_data.delete(id)
        end
      }
    end

    # callback to call a Ruby method from JavaScript
    add_action_callback("_callTo"){|dlg, method_name, *arguments|
      # TODO: support of instance methods in namespace outside this WebDialogX class
      eval("#{method_name}(#{arguments.inspect[1..-2]})", @scope) # no return, no rescue (errors will be caught by caller)
    }

    # callback to execute any Ruby code from JavaScript
    add_action_callback("_execScript"){|dlg, code_string|
      eval(code_string, @scope) # no return, no rescue (errors will be caught by caller)
    }
  end

=begin
  # JavaScript injection disabled; Dialog.js is included in html document
  def show(&block)
    # TODO: how to inject this script if block does not trigger (OS X/Safari)
    # workaround: place the script snippet directly in your webdialog html or js
    super{
      script = load_javascript()
      execute_script(script)
      block.call if block_given?
    }
  end


  def show_modal(&block)
    # TODO: how to inject this script if block does not trigger (OS X/Safari)
    # workaround: place the script snippet directly in your webdialog html or js
    super{
      script = load_javascript()
      execute_script(script)
      execute_script("Dialog.debug = #{@debug}")
      block.call if block_given?
    }
  end
=end

  def set_element_value(element_id, value)
    execute_script("document.getElementById('#{element_id}').value = #{value.inspect}")
  end


  def call_to(function_name, *arguments, &block)
    # Avoid collision with JavaScript message IDs by using set of negative numbers.
    id = (@message_id_counter -= 1)
    arguments_string = to_json(arguments)
    execute_script("Dialog._fromRuby(#{id}, \"#{function_name}\", \"#{arguments_string.inspect[1..-2]}\")")
    return_value = (self.visible?)? from_json( get_element_value("Dialog.#{id}") ) : nil
    execute_script("Dialog._cleanUp(#{id})")
    block.call(self, return_value) if block_given?
    return return_value
  end



  def exec_script(code_string, &block)
    call_to("this._execScript", code_string, &block)
  end


  def debug
    return @debug
  end


  def debug=(boolean)
    @debug = boolean
    execute_script("Dialog.debug = #{@debug}") if visible?
  end


  private


  def load_javascript
    # resolve relative pathname
    dir = File.dirname(__FILE__)
    file = File.expand_path(File.join(dir, @@javascript_file))
    IO.read(file)
  end



  def from_json(json_string)
    # split at every even number of unescaped quotes; if it's not a string then replace : and null
    # DEBUG: test objects with empty strings "", test arrays, test pure strings, test strings with : or => or " or \
    ruby_string = json_string.split(/(\"(?:.*?[^\\])*?\")/).collect{|s| (s[0..0] != '"')? s.gsub(/\:/, "=>").gsub(/null/, "nil") : s }.join()
    result = eval(ruby_string)
    return result
    rescue Exception => e
      {}
  end


  def to_json(obj)
    # remove non-JSON objects
    return "null" unless [String, Symbol, Fixnum, Float, Array, Hash, TrueClass, FalseClass, NilClass].inject(false){|b,c| b = (obj.is_a?(c))? true : b}
    if obj.is_a? Array
      obj.reject!{|k| ![String, Symbol, Fixnum, Float, Array, Hash, TrueClass, FalseClass, NilClass].inject(false){|b,c| b = (k.is_a?(c))? true : b} }
    elsif obj.is_a? Hash
      obj.reject!{|k,v|
        !k.is_a?(String) && !k.is_a?(Symbol) ||
        ![String, Symbol, Fixnum, Float, Array, Hash, TrueClass, FalseClass, NilClass].inject(false){|b,c| b = (v.is_a?(c))? true : b}
      }
    end
    # split at every even number of unescaped quotes; if it's not a string then turn Symbols into String and replace => and nil
    json_string = obj.inspect.split(/(\"(?:.*?[^\\])*?\")/).collect{|s| (s[0..0] != '"')? s.gsub(/\:(\S+?(?=\=>|\s))/, "\"\\1\"").gsub(/=>/, ":").gsub(/\bnil\b/, "null") : s }.join()
    return json_string
  end


end
