module Ray
  module GL
    class Vertex
      include Ray::PP

      @vertex_classes = {0 => Ray::Vertex}

      class Instance
        include Ray::PP

        @instance_classes = {}

        class << self
          attr_reader :instance_classes
        end

        def self.layout
          @vertex_instance_layout
        end

        def to_s
          pairs = []
          self.class.layout.each do |key, _, _, per_instance|
            pairs << "#{key}=#{send(key)}" if per_instance
          end

          "#<#{self.class} #{pairs.join " "}>"
        end

        def pretty_print(q)
          attr = []
          self.class.layout.each do |key, _, _, per_instance|
            attr << key if per_instance
          end

          pretty_print_attributes q, attr
        end
      end

      # Creates a new Vertex class with a custom layout. Layout is an array of
      # arrays, where each row contains 3 or 4 elements:
      #
      # 1. Attribute name in Ruby
      # 2. Attribute name in GLSL shaders
      # 3. Attribute type, one of the following symbols: float, int, ubyte,
      #    bool, color, vector2, vector3.
      # 4. Whether the attribute is only accessibel on a per-instance basis (as
      #    opposed to per-vertex, which is the default)
      #
      # Getters and setters are created for all of the attributes.
      #
      # @param [Array] layout
      #
      # @example
      #   TestVertex = Ray::GL::Vertex.make [
      #     [:float,   "in_Float",    :float],
      #     [:int,     "in_Int",      :int],
      #     [:ubyte,   "in_Ubyte",    :ubyte],
      #     [:bool,    "in_Bool",     :bool],
      #     [:color,   "in_Color",    :color],
      #     [:vector2, "in_Vector2",  :vector2],
      #     [:vector3, "in_Vector3",  :vector3],
      #
      #     # per-instance data
      #     [:instance_color, "magic", :color, true]
      #   ]
      def self.make(layout)
        layout.each do |_, _, type, _|
          unless TypeMap.has_key? type
            raise ArgumentError, "unknown type in a vertex: #{type.inspect}"
          end
        end

        vtype = make_type layout.map { |_, *rest| rest }

        @vertex_classes[vtype] = Class.new self do
          # Be *very* careful with those values.
          @vertex_type_id     = vtype
          @vertex_type_size   = Vertex.size(vtype)
          @vertex_type_layout = layout

          if layout.any? { |_, _, _, per_instance| per_instance }
            const_set :Instance, Class.new(Ray::GL::Vertex::Instance) {
              @vertex_instance_type_id = vtype
              @vertex_instance_size    = Vertex.instance_size(vtype)
              @vertex_instance_layout  = layout

              class << self
                undef instance_classes
              end

              Vertex.define_layout self, layout, vtype, true
            }
          end

          class << self
            undef make
            undef make_type
            undef size
            undef offset_of
          end

          Vertex.define_layout self, layout, vtype, false
        end

        if @vertex_classes[vtype].const_defined? :Instance
          Instance.instance_classes[vtype] = @vertex_classes[vtype]::Instance
        end

        @vertex_classes[vtype]
      end

      def self.layout
        @vertex_type_layout
      end

      def self.default_for(type)
        case type
        when :float, :int, :ubyte then 0
        when :bool                then true
        when :vector2             then Ray::Vector2[0, 0]
        when :vector3             then Ray::Vector3[0, 0, 0]
        when :color               then Ray::Color.white
        end
      end

      def self.define_layout(on, layout, vtype, instance = false)
        argument_layout = layout.reject do |_, _, _, per_instance|
          per_instance ^ instance
        end

        on.class_eval do
          define_method :initialize do |*args|
            if args.size > layout.size
              msg = "wrong number of arguments: #{args.size} for" <<
                " #{layout.size}"
              raise ArgumentError, msg
            end

            argument_layout.each_with_index do |(attr, _, type, _), i|
              send("#{attr}=", args[i] || Vertex.default_for(type))
            end
          end

          layout.each_with_index do |(attr, _, type, per_instance), i|
            next if per_instance ^ instance

            offset = Vertex.offset_of(vtype, i)

            # Faster than define_method.
            module_eval(<<-eom, __FILE__, __LINE__)
              def #{attr}
                element(#{offset}, #{type.inspect})
              end

              def #{attr}=(val)
                set_element(#{offset}, #{type.inspect}, val)
              end
            eom

            alias_method "#{attr}?", attr if type == :bool
          end
        end
      end

      def to_s
        pairs = []
        self.class.layout.each do |key, _, _, per_instance|
          pairs << "#{key}=#{send(key)}" unless per_instance
        end

        "#<#{self.class} #{pairs.join " "}>"
      end

      def pretty_print(q)
        attr = []
        self.class.layout.each do |key, _, _, per_instance|
          attr << key unless per_instance
        end

        pretty_print_attributes q, attr
      end
    end
  end
end
