module Laser
  # Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
  # their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
  # to, for example, an array without those additions being shared with either their parent, siblings, or
  # children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
  module InheritedAttributes # :nodoc:
    def self.extended(base)
      return if const_defined?(:Rails)
      base.__send__(:extend, ClassMethods)
      class << base
        alias inherited_without_inheritable_attributes inherited
        alias inherited inherited_with_inheritable_attributes
      end
    end
    module ClassMethods
      def class_inheritable_reader(*syms)
        syms.each do |sym|
          next if sym.is_a?(Hash)
          class_eval(<<-EOS, __FILE__, __LINE__ + 1)
            def self.#{sym}                                # def self.after_add
              read_inheritable_attribute(:#{sym})          #   read_inheritable_attribute(:after_add)
            end                                            # end
                                                           #
            def #{sym}                                     # def after_add
              self.class.#{sym}                            #   self.class.after_add
            end                                            # end
          EOS
        end
      end

      def class_inheritable_writer(*syms)
        syms.each do |sym|
          next if sym.is_a?(Hash)
          class_eval(<<-EOS, __FILE__, __LINE__ + 1)
            def self.#{sym}=(obj)                          # def self.color=(obj)
              write_inheritable_attribute(:#{sym}, obj)    #   write_inheritable_attribute(:color, obj)
            end                                            # end
                                                           #
            def #{sym}=(obj)                               # def color=(obj)
              self.class.#{sym} = obj                      #   self.class.color = obj
            end                                            # end
          EOS
        end
      end

      def class_inheritable_array_writer(*syms)
        syms.each do |sym|
          next if sym.is_a?(Hash)
          class_eval(<<-EOS, __FILE__, __LINE__ + 1)
            def self.#{sym}=(obj)                          # def self.levels=(obj)
              write_inheritable_array(:#{sym}, obj)        #   write_inheritable_array(:levels, obj)
            end                                            # end
                                                           #
            def #{sym}=(obj)                               # def levels=(obj)
              self.class.#{sym} = obj                      #   self.class.levels = obj
            end                                            # end
          EOS
        end
      end

      def class_inheritable_hash_writer(*syms)
        syms.each do |sym|
          next if sym.is_a?(Hash)
          class_eval(<<-EOS, __FILE__, __LINE__ + 1)
            def self.#{sym}=(obj)                          # def self.nicknames=(obj)
              write_inheritable_hash(:#{sym}, obj)         #   write_inheritable_hash(:nicknames, obj)
            end                                            # end
                                                           #
            def #{sym}=(obj)                               # def nicknames=(obj)
              self.class.#{sym} = obj                      #   self.class.nicknames = obj
            end                                            # end
          EOS
        end
      end

      def class_inheritable_accessor(*syms)
        class_inheritable_reader(*syms)
        class_inheritable_writer(*syms)
      end

      def class_inheritable_array(*syms)
        class_inheritable_reader(*syms)
        class_inheritable_array_writer(*syms)
      end

      def class_inheritable_hash(*syms)
        class_inheritable_reader(*syms)
        class_inheritable_hash_writer(*syms)
      end

      def inheritable_attributes
        @inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES
      end

      def write_inheritable_attribute(key, value)
        if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
          @inheritable_attributes = {}
        end
        inheritable_attributes[key] = value
      end

      def write_inheritable_array(key, elements)
        write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
        write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
      end

      def write_inheritable_hash(key, hash)
        write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil?
        write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
      end

      def read_inheritable_attribute(key)
        inheritable_attributes[key]
      end

      def reset_inheritable_attributes
        @inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
      end

      private
        # Prevent this constant from being created multiple times
        EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES)

        def duplicable?(x)
          ![NilClass, FalseClass, TrueClass, Module, ::Symbol, Numeric].any? do |klass|
            x.is_a?(klass)
          end
        end

        def inherited_with_inheritable_attributes(child)
          inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes)

          if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
            new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
          else
            new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)|
              memo.update(key => duplicable?(value) ? value.dup : value)
            end
          end

          child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes)
        end
    end
  end
end
