=begin
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:         Dialog.rb
Author:       Andreas Eisenbarth
Description:  Subclass for UI::WebDialog.
              This subclass implements a communication system between Ruby.
Usage:

Create an instance (with the same arguments as UI::WebDialog):

    dialog = Dialog.new(*args)

Add an event handler when the dialog is shown: (show{} was unreliable in some SketchUp versions)

    dialog.on_show{ }

Add an event handler/callback, that receives any amount of arguments of any JSON type:

    dialog.on(String){ |dlg, *args| }

Remove an event handler/callback:

    dialog.off(String)

Call a JavaScript callback from a Ruby callback synchronously or asynchronously,
with any amount of arguments. You can pass this 'dlg' reference through any
methods or code blocks and call it later:

    dlg.return(*args)

Call a public JavaScript function with any amount of arguments of any JSON type:

    dialog.call_function(String, *args)

Requires:     JavaScript module AE.Bridge.
              Call AE.Bridge.initialize() at the end of your HTML document.
Version:      1.1.5
Date:         08.05.2014
=end


# Prefer to use json lib if available (optional).
#Sketchup::require "json" # Raises no error, but displays in load errors popup.
if Sketchup.version.to_i >= 14
  begin; load "json.rb" unless defined?(JSON); rescue LoadError; end
end


module AE


module ToolbarEditor


# A subclass of UI::WebDialog.
class Dialog < UI::WebDialog


# Give a short string for inspection. This does not output instance variables
# since these contain a lot of data, references to other objects and self-references.
#
# @return [String] the instance's class and object id
def inspect
  return "#<#{self.class}:0x#{(self.object_id << 1).to_s(16)}>"
end


@@reserved_callbacks = ["AE.Bridge.receive_message", "AE.Bridge.initialize"]
def initialize(*args)
  # messaging variables
  @callbacks  =  {} # Hash of callback_name => Proc
  @procs_show =  [] # Array of Procs
  @procs_close = [] # Array of Procs
  @visible = false

  # Tell SketchUp that the document is shown and completely loaded.
  # This needs to be invoked by doing in JavaScript:
  # AE.Bridge.initialize();
  @callbacks["AE.Bridge.initialize"] = Proc.new{ |dialog|
    # Set constants for JavaScript module AE
    dialog.execute_script(
      "AE.RUBY_VERSION = #{RUBY_VERSION.to_f};" <<
      "AE.SKETCHUP_VERSION = #{Sketchup.version_number.to_f};"
    )
    # Trigger all event handlers for when the dialog is shown.
    # Output errors because SketchUp's native callback would not output errors.
    @procs_show.each{ |block|
      begin
        block.call(dialog)
      rescue Exception => e
        if defined?(AE::Console)
          AE::Console.error(e)
        else
          $stderr.write(e.message << $/)
          $stderr.write(e.backtrace.join($/) << $/)
        end
      end
    }
  }

  # Try to set the default dialog color as background. # TODO Test on Windows 7 and OS X if this is still necessary.
  # This is a workaround because on some systems/browsers the CSS system color is
  # wrong (white), and on some systems SketchUp returns white (also mostly wrong).
  @procs_show << Proc.new{ |dialog|
    color = dialog.get_default_dialog_color
    # If it's white, then it is likely not correct and we try the CSS system color instead.
    if color != "#ffffff"
      script = "if (!document.body.style.background && " +
        "!document.body.style.backgroundColor) { " +
        "document.body.style.backgroundColor = '#{color}'; }"
      dialog.execute_script(script)
    end
  }

  # Puts (for debugging)
  @callbacks["puts"] = Proc.new{ |dialog, *params|
    puts(*params.map{ |param| param.inspect })
  }

  # Error channel (for debugging)
  @callbacks["error"] = Proc.new{ |dialog, param|
    if defined?(AE::Console)
      AE::Console.error(param.inspect)
    else
      $stderr.write(param.inspect << $/)
    end
  }

  super(*args)

  # SketchUp does not release procs of WebDialogs. Because of that, we need to
  # make sure that the proc contains no reference to this instance. The proc
  # receives a reference to this dialog, so it can call the follow-up method #action_callback.
  self.add_action_callback("AE.Bridge.receive_message", &@@action_callback_wrapper)

  # However, the proc of set_on_close does not receive a reference to the dialog.
  # So we need to tell it the dialog without passing an actual reference to the
  # dialog (which would not garbage-collect). We do this with the object id (Fixnum).
  self.class.set_on_close_wrapper(self.object_id)
end


### Messaging related methods ###


# SketchUp does not release procs of WebDialogs. Because of that, we need to
# make sure that the proc contains no reference to this instance. For all webdialogs,
# we can use this single proc, because it receives a reference to the dialog.
# Then it calls the method #action_callback on the dialog which knows the other callbacks.
@@action_callback_wrapper = Proc.new{ |dlg, param| dlg.action_callback_wrapper(dlg, param) }


# However, the proc of set_on_close does not receive a reference to the dialog.
# So we need to tell it the dialog without passing an actual reference to the
# dialog (which would not garbage-collect). We do this with the object id (Fixnum).
# @private
# @param [Fixnum] id
def self.set_on_close_wrapper(id)
  ObjectSpace._id2ref(id).set_on_close{
    # This proc does not garbage-collect. It binds the reference 'id' which is
    # not so bad because it is just a (leight-weight) Fixnum.
    ObjectSpace._id2ref(id).close()
  }
end


# Receives the raw messages from the WebDialog (AE.Bridge.callRuby) and calls the individual callbacks.
# @private - Not for public use.
# @param [UI::WebDialog] dlg
# @param [String] param
def action_callback_wrapper(dlg, param)
  begin
    # Get message data from the hidden input element.
    value = dlg.get_element_value("AE.Bridge.messageField")
    data = from_json(value)
    raise(ArgumentError, "Dialog received wrong data: \n#{value}") unless
      data.is_a?(Hash) &&
      data["id"].is_a?(Fixnum) &&
      data["name"].is_a?(String) &&
      data["arguments"].is_a?(Array)
    id = data["id"]
    name = data["name"]
    arguments = data["arguments"] || []

    # Get the callback.
    raise(ArgumentError, "Callback '#{name}' for #{dlg} not found.") if name.nil? || !@callbacks.include?(name)
    callback = @callbacks[name]

    # Run the callback
    # Here we pass a wrapper around this dialog which preserves the message id
    # to identify the corresponding JavaScript callback.
    # This allows to run asynchronous code (external application etc.) and return
    # later the result to the JavaScript callback even if the dialog has continued
    # sending/receiving messages.
    message = Message.new(dlg, id)
    begin
      callback.call(message, *arguments)
    rescue Exception => e
      # TODO: It could tell JavaScript that there was an error
      # TODO: This raises mainly errors from within the proc (other file), not from calling the proc (this file).
      raise(e.class, "#{self.class.to_s} Error for callback '#{name}': \n#{e.message}", e.backtrace)
    end

  rescue Exception => e
    if defined?(AE::Console)
      AE::Console.error(e)
    else
      $stderr.write(e.message << $/)
      $stderr.write(e.backtrace.join($/) << $/)
    end
  ensure
    # Unlock JavaScript to send the next message.
    dlg.execute_script("AE.Bridge.nextMessage()")
  end
end


# Wrapper class around the dialog so that Ruby callbacks can remember the
# corresponding JavaScript callback id.
class Message

  attr_reader :dialog

  def initialize(dialog, id)
    @dialog = dialog
    @message_id = id
  end

  def return(*arguments)
    arguments_string = @dialog.__send__(:to_json, arguments)[1...-1]
    arguments_string = "null" if arguments_string.nil? || arguments_string.empty?
    execute_script("AE.Bridge.callbackJS(#{@message_id}, #{arguments_string})")
  end


  def method_missing(method_name, *arguments)
    return @dialog.__send__(method_name, *arguments)
  end

end


# Add a callback handler.
# @param [String] callback_name
# @param [Proc] block
def on(callback_name, &block)
  raise(ArgumentError, "Argument 'callback_name' must be a String.") unless callback_name.is_a?(String)
  raise(ArgumentError, "Argument 'callback_name' can not be '#{callback_name}'.") if @@reserved_callbacks.include?(callback_name)
  raise(ArgumentError, "Must have a Proc.") unless block_given?
  @callbacks[callback_name] = block
  return self
end


# Remove a callback handler.
# @param [String] callback_name
def off(callback_name)
  raise(ArgumentError, "Argument 'callback_name' must be a String.") unless callback_name.is_a?(String)
  @callbacks.delete(callback_name)
  return self
end


# Add event handlers for when the dialog is shown.
# @param [Proc] block to execute when the dialog becomes visible
def on_show(&block)
  raise(ArgumentError, "Must have a Proc.") unless block_given?
  @procs_show << block
  return self
end


# Add event handlers for when the dialog is closed.
# @param [Proc] block to execute when the dialog is closed
def on_close(&block)
  raise(ArgumentError, "Must have a Proc.") unless block_given?
  @procs_close << block
  return self
end


# Shows an "always on top" dialog, which is called not modal on Windows but modal on OSX.
# @param [Proc] block to execute when the dialog shows.
def show(&block)
  if ( Object::RUBY_PLATFORM =~ /darwin/i )
    show_modal(&block)
  else
    super
  end
  @visible = true
end


# Closes the dialog and runs all on_close handlers.
def close
  if @visible
    @visible = false
    @procs_close.each{ |block|
      begin
        block.call(self)
      rescue Exception => e
        if defined?(AE::Console)
          AE::Console.error(e)
        else
          $stderr.write(e.message << $/)
          $stderr.write(e.backtrace.join($/) << $/)
        end
      end
    }
    super
  end
end


# Call a JavaScript function with JSON arguments in the webdialog.
# @param [String] name of a public JavaScript function
# @params [Object] arguments array of JSON-compatible objects
def call_function(name, *arguments)
  arguments.map!{ |arg| to_json(arg) }
  execute_script("#{name}(#{arguments.join(", ")});")
end


### Helper methods ###


# Read a JSON string and return a hash.
# @param [String] json_string
# @returns [Hash,Array]
# TODO: undefined is not allowed to occur in JSON, but in case it happens not sure what to do?
def from_json(json_string)
  raise(ArgumentError, "Argument 'json_string' must be a String.") unless json_string.is_a?(String)
  
  # Use JSON lib if available.
  # Only generation of JSON objects or arrays allowed, so we wrap it into an array (it could be a String etc.).
  return JSON.parse("["+json_string+"]").first if defined?(JSON) # Use json library if available.
  
  # Otherwise we use inspect and string manipulation.
  # Split at every even number of unescaped quotes.
  # If it's not a string then replace : and null
  ruby_string = json_string.split(/(\"(?:.*?[^\\])*?\")/).
    map{ |s|
      (s[0..0] != '"')? s.gsub(/\:/, "=>").gsub(/null/, "nil").gsub(/undefined/, "nil") : s
    }.
    join()
  result = eval(ruby_string)
  return result
rescue Exception => e
  if defined?(AE::Console)
    AE::Console.error(e)
  else
    $stderr.write(e.message << $/)
    $stderr.write(e.backtrace.join($/) << $/)
  end
end
private :from_json


# This converts Ruby objects into JSON.
# @params [Hash,Array,String,Numeric,Boolean,NilClass] obj
# @returns [String] JSON string
def to_json(obj)
  json_classes = [String, Symbol, Fixnum, Float, Array, Hash, TrueClass, FalseClass, NilClass]
  # Remove non-JSON objects.
  sanitize = nil
  sanitize = Proc.new{ |v|
    if v.is_a?(Array)
      new_v = []
      v.each{ |a| new_v << sanitize.call(a) if json_classes.include?(a.class) }
      new_v
    elsif v.is_a?(Hash)
      new_v = {}
      v.each{ |k, w| new_v[k.to_s] = sanitize.call(w) if (k.is_a?(String) || k.is_a?(Symbol)) && json_classes.include?(w.class) }
      new_v
    elsif v.is_a?(Symbol)
      v.to_s
    else
      v
    end
  }
  if json_classes.include?(obj.class)
    o = sanitize.call(obj)
  else
    return "null"
  end
  
  # Use JSON lib if available.
  # Only generation of JSON objects or arrays allowed, so we wrap it into an array (it could be a String etc.).
  return JSON.generate([o])[1...-1] if defined?(JSON)
  
  # Otherwise we use inspect and string manipulation.
  # Split at every even number of unescaped quotes. This gives either strings
  # or what is between strings.
  # Replace => and nil.
  json_string = o.inspect.split(/(\"(?:.*?(?:[\\][\\]+?|[^\\]))*?\")/).
    map{ |s|
      (s[0..0] != '"')?                        # If we are not inside a string
      s.gsub(/\=\>/, ":").                     # Arrow to colon
        gsub(/\bnil\b/, "null") :              # nil to null
      s
    }.join
  return json_string
end
private :to_json


end # class Dialog


end


end # module AE
