# encoding: utf-8

module Watir
  module HTML
    class Visitor < WebIDL::RubySexpVisitor

      def initialize
        super

        # When an interface has multiple IDL definitions in the spec, the inheritance is sometimes
        # not repeated. So we'll keep track ourselves.
        @inheritance_map = {}

        @already_defined = []
      end

      #
      # WebIDL visitor interface
      #

      def visit_interface(interface)
        name   = interface.name
        parent = interface.inherits.first

        $stderr.puts name
        return unless name =~ /^HTML/ && name !~ /(Collection|Document)$/

        if name == "HTMLElement"
          parent = 'Element'
        elsif parent
          @inheritance_map[name] ||= parent.name
          parent = parent.name
        else
          parent = @inheritance_map[name] || return
        end

        [ :scope,
          [:block,
            element_class(interface.name, interface.members.select { |e| e.kind_of?(WebIDL::Ast::Attribute) }, parent),
            collection_class(interface.name)
          ]
        ]
      end

      def visit_module(mod)
        # ignored
      end

      def visit_implements_statement(stmt)
        # ignored
      end

      # TODO: do everything in the visitor somehow?
      # problem is we lack the tag name info while walking the interface AST
      # #
      # # Watir generator visitor interface
      # #
      #
      # def visit_tag(tag_name, interface_name)
      #   tag_string       = tag.inspect
      #   singular         = Util.paramify(tag)
      #   plural           = singular.pluralize
      #   element_class    = Util.classify(interfaces.first.name)
      #   collection_class = "#{element_class}Collection"
      #
      #   [:defn,
      #    :a,
      #    [:args, :"*args"],
      #    [:scope,
      #     [:block,
      #      [:call,
      #       [:const, :Anchor],
      #       :new,
      #       [:arglist,
      #        [:self],
      #        [:call,
      #         [:call, nil, :extract_selector, [:arglist, [:lvar, :args]]],
      #         :merge,
      #         [:arglist, [:hash, [:lit, :tag_name], [:str, "a"]]]]]]]]]
      # end

      private

      def element_class(name, attributes, parent)
        [:class, Util.classify(name), [:const, Util.classify(parent)],
          [:scope,
            [:block, attributes_call(attributes)]
          ]
        ]
      end

      def collection_class(name)
        return if @already_defined.include?(name)
        @already_defined << name
        name = Util.classify(name)

        [:class, "#{name}Collection", [:const, :ElementCollection],
          [:scope,
            [:defn, :element_class,
              [:args],
              [:scope,
                [:block, [:const, name]]
              ]
            ]
          ]
        ]
      end

      def attributes_call(attributes)
        return if attributes.empty?

        attrs = Hash.new { |hash, key| hash[key] = [] }
        attributes.each do |a|
          attrs[ruby_type_for(a.type)] << a.name.snake_case
        end

        call :attributes, [[:hash] + attrs.map { |type, names| [[:lit, type], literal_array(names)] }.flatten(1)]
      end

      def identifier_call(tag_name)
        call :identifier, [literal_hash(:tag_name => tag_name)]
      end

      def container_call(name)
        call :container_method,  [[:lit, Util.paramify(name).to_sym]]
      end

      def collection_call(name)
        call :collection_method, [[:lit, Util.paramify(name).pluralize.to_sym]]
      end

      def literal_hash(hash)
        [:hash] + hash.map { |k, v| [[:lit, k.to_sym], [:lit, v]] }.flatten(1)
      end

      def literal_array(arr)
        [:array] + arr.map { |e| [:lit, e.to_sym] }
      end

      def call(name, args)
        [:call, nil, name.to_sym, [:arglist] + args]
      end

      def ruby_type_for(type)
        case type.name.to_s
        when 'DOMString', 'any'
          :string
        when 'UnsignedLong', 'Long', 'Integer', 'Short', 'UnsignedShort'
          :int
        when 'Float', 'Double'
          :float
        when 'Function'
          :function
        when 'Boolean'
          :bool
        when 'Document'
          :document
        when 'DOMTokenList', 'DOMSettableTokenList'
          :token_list
        when 'DOMStringMap'
          :string_map
        when 'HTMLPropertiesCollection'
          :properties_collection
        when /HTML(.*)Element/
          :html_element
        when /HTML(.*)Collection/
          :html_collection
        when 'CSSStyleDeclaration'
          :style
        when /.+List$/
          :list
        when 'Date'
          :date
        when 'Element'
          :element
        when 'WindowProxy', 'ValidityState', 'MediaError', 'TimeRanges', 'Location', 'Any', 'TimedTrackArray', 'TimedTrack'
          # probably completely wrong.
          :string
        else
          raise "unknown type: #{type.name}"
        end
      end

    end # Visitor
  end # Support
end # Watir