module SwingSupport
  module Extensions
    # Module allows including classes to receive attribute values in an *opts* Hash
    # and sets those attributes after object initialization
    module Attributes
      def self.included host
        host.send :extend, ClassMethods
        host.instance_eval do
#          attr_setter :font, :tool_tip_text, :enabled
          alias :new_without_attributes :new
          alias :new :new_with_attributes
        end
      end

      # Post-processing (non-setter) options given to initialize
      # You need to delete processed/consumed options from given Hash
      def post_process opts
        attach_to opts.delete(:parent)
      end

      # Proper way to add generic component to its parent
      def attach_to parent
        parent.add self if parent
      end

      module ClassMethods
        def attributes
          @attributes ||= (superclass.attributes.dup rescue {})
        end

        # Adds settable attributes for a given class, possibly with defaults
        # If defaults are given for attributes, they should be put at the end (as opts)
        def attr_setter *new_attributes
          if new_attributes.last.is_a? Hash
            # Some attributes are given with defaults
            new_attributes_with_defaults = new_attributes.pop
            new_attributes_with_defaults.each { |name, default| attributes[name] = default }
          end
          new_attributes.each { |name| attributes[name] = nil }
        end

        # Sets attributes after calling original new
        def new_with_attributes(*args, &block)
          opts = args.last.is_a?(Hash) ? args.pop.dup : {}
          component = self.new_without_attributes(*args, &block)

          # Extract known attributes given in opts,
          # run default actions on them, or return known defaults
          attributes = attributes().map do |name, default|
            value = opts.delete name
            result = if default.nil? # No default, use value directly
                       value
                     elsif default.respond_to? :call # Default is callable, call it with whatever value
                       default.call *value
                     elsif default.is_a?(Class) && value.class != default # Default class of this attribute, create new
                       default.new *value unless value.nil?
                     else # Return either non-nil value or default
                       value.nil? ? default : value
                     end
            [name, result] unless result.nil?
          end.compact

          attributes.each do |(name, value)|
            if component.respond_to? "#{name}="
              component.send "#{name}=", *value
            elsif component.respond_to? "set_#{name}"
              component.send "set_#{name}", *value
            else
              raise ArgumentError.new "Setter #{name} does not work for #{component}"
            end
          end

          # Post-process non-setter opts (setter opts are already consumed by now)
          component.post_process opts

          # Raises exception if any of the given options left unprocessed
          raise "Unrecognized options: #{opts}" unless opts.empty?

          component
        end
      end
    end
  end
end