module AE


module ToolbarEditor


# Translation library.
require(File.join(DIR, 'Translate.rb'))
# Load translation strings.
TRANSLATE = Translate.new("ToolbarEditor", File.join(DIR, "resources")) unless defined?(self::TRANSLATE)


# An index of SketchUp's commands.
# This is a small subset of AE::LaunchUp::Index.
#
class Index


  # Make this a singleton class.
  private_class_method :new

  class << self

    def instance
      if @__instance__.nil?
        @__instance__ = self.__send__(:new)
        def instance; return @__instance__; end
      end
      return @__instance__
    end

  end # class << self


  # Instance methods


  # Create a new instance of the index and fill it with all existing UI::Commands.
  #
  def initialize
    @data = {}

    # Optimization: cache toolbar names in a hash to speed up Index.add()
    # Note: Iterating over toolbars can give a UI::Command or String (separator "|").
    @toolbars = {}
    ObjectSpace.each_object(UI::Toolbar){ |toolbar|
      toolbar.each{ |command|
        next unless command.is_a?(UI::Command)
        # TODO: Command from toolbar is different reference and cannot be matched.
        # Workaround: Use hash_code instead or menu_text?
        # @toolbars[command] = toolbar.name
        @toolbars[command.menu_text] = toolbar.name
      } if toolbar.respond_to?(:each)
    }

    # Load all existing UI::Commands into the index.
    ObjectSpace.each_object(UI::Command){ |command|
      add(command)
    }
  end


  # This adds a new entry to the index. It requires one or both of these arguments in any order:
  # @param [UI::Command] command
  # @param [Hash] hash  with metadata
  #
  # The following metadata is available and overrides those of the UI::Command:
  #   :command [UI::Command]
  #   :name [String] A title of the command. It should allow the user to identify
  #       the command (unambiguous) and include the significant aspect of this
  #       command (ie. "Unselect groups" instead of "Groups").
  #       It corresponds to the String argument of UI::Command.new, or menu_text.
  #   :description [String] A full phrase or phrases describing what the command does.
  #       It corresponds to status_bar_text or tooltip (but more general-purpose).
  #   :icon [String] An absolute file path to a (png) icon image.
  #   :category [String] (optional) A term for grouping the command with others,
  #       ie. the field of application, toolbar name or menu path.
  #
  # Private metadata:
  #   :id [Fixnum] This is a unique hash code that makes commands identifiable
  #       over sessions. (because UI::Command.object_id is lost)
  #
  def add(*args)
    hash = args.grep(Hash).first || {}
    command = args.grep(UI::Command).first || hash[:command]
    raise(ArgumentError, "Argument 'command' must be a UI::Command.") unless command.is_a?(UI::Command) || command.nil?

    if command.nil?
      return
    # Add meta data from command to hash.
    else # if command.is_a?(UI::Command)

      # Name
      hash[:name] = command.menu_text

      # Description
      hash[:description] ||= command.respond_to?(:status_bar_text) ?
        command.status_bar_text || command.tooltip : nil

      # Icon
      # Note: If developer assigns a relative file path, SketchUp interprets it
      # relative to the file where it has been assigned command.large_icon=() and
      # converts it into an absolute path. So the getter method command.large_icon
      # gets always an absolute path.
      if !hash[:icon] || hash[:icon].empty?
        hash[:icon] = File.expand_path(command.large_icon) if command.respond_to?(:large_icon) && !command.large_icon.empty?
        hash[:icon] ||= File.expand_path(command.small_icon) if command.respond_to?(:small_icon) && !command.small_icon.empty?
      end

      # Category
      # A category defines more clearly the context of a command (if the name is unspecific).
      if !hash[:category] && command.respond_to?(:category)
        hash[:category] ||= command.category
      elsif !hash[:category] && @toolbars.include?(command.menu_text)
        hash[:category] ||= TRANSLATE[@toolbars[command.menu_text]]
      end

      # Keep a reference to the original command.
      hash[:command] = command

    end # if command.is_a?(UI::Command)

    # Create a short id to distinguish it from other commands.
    id = hash_code(hash[:name].to_s + hash[:description].to_s + hash[:icon].to_s)
    hash[:id] = id

    # Add this entry only if it is not already contained.
    # Assume that an existing entry is equivalent and update it.
    if @data.include?(id)
      update(hash)
    else
      @data[id] = hash
    end
    return true
  rescue ArgumentError
    raise
  rescue StandardError => e
    e.message << " when adding #{command} to index."
    if defined?(AE::Console)
      AE::Console.error(e)
    else
      $stderr.write(e.message << $/)
      $stderr.write(e.backtrace.join($/) << $/)
    end
    return false
  end


  # This method changes an entry in the index.
  # @params [Hash] hash of new keys and values.
  #     It should have a :id or :command to identify an existing entry in the index.
  # @returns [Boolean] success
  def update(hash)
    # Try to find an existing entry in @data.
    if hash[:id].is_a?(Numeric)
      entry = @data[hash[:id]]
    elsif hash[:command].is_a?(UI::Command)
      entry = @data.values.find{ |e| e[:command] == hash[:command] }
    end
    return false unless entry
    entry.merge!(hash)
    return true
  end


  # Get a command by its ID.
  # @param [Fixnum] id
  # @returns [Hash] entry
  def get_by_id(id)
    raise(ArgumentError, "Argument 'id' must be a Fixnum") unless id.is_a?(Fixnum)
    return @data[id]
  end
  alias_method(:[], :get_by_id)


  # Get all commands.
  # @param [Proc] block optional conditional (true/false) of which commands should be returned.
  # @returns [Array] entries
  def get_all(&block)
    return (block_given?) ? @data.values.select(&block) : @data.values
  end


  private


  # Hash function.
  # Since object_ids are not persistent and commands have long names of varying
  # length, we use a hash function to get short and reproducible identifiers.
  # Tip: Use strings and never objects whose <inspect> value contains the <object_id>.
  # @param [Object] object
  def adler32(s)
    s = s.inspect unless s.is_a?(String)
    h = 0
    m = 99999
    s.unpack("c*").each{ |c| h = (128 * h + c).modulo(m)}
    return h
  end
  alias_method :hash_code, :adler32


end # module Index


end # module ToolbarEditor


end # module AE
