module Watir
  # Base class for html elements.
  # This is not a class that users would normally access.
  class Element # Wrapper
    include ElementExtensions
    include Exception
    include Container # presumes @container is defined
    attr_accessor :container

    # number of spaces that separate the property from the value in the to_s method
    TO_S_SIZE = 14

    # ole_object - the ole object for the element being wrapped
    def initialize(ole_object)
      @o = ole_object
      @original_color = nil
    end

    # Return the ole object, allowing any methods of the DOM that Watir doesn't support to be used.
    def ole_object # BUG: should use an attribute reader and rename the instance variable
      return @o
    end

    def ole_object=(o)
      @o = o
    end

    def inspect
      '#<%s:0x%x located=%s how=%s what=%s>' % [self.class, hash*2, !!ole_object, @how.inspect, @what.inspect]
    end

    private
    def self.def_wrap(ruby_method_name, ole_method_name=nil)
      ole_method_name = ruby_method_name unless ole_method_name
      class_eval "def #{ruby_method_name}
                          assert_exists
                          ole_object.invoke('#{ole_method_name}')
                        end"
    end

    def self.def_wrap_guard(method_name)
      class_eval "def #{method_name}
                          assert_exists
                          begin
                            ole_object.invoke('#{method_name}')
                          rescue
                            ''
                          end
                        end"
    end


    public
    def assert_exists
      locate if respond_to?(:locate)
      unless ole_object
        raise UnknownObjectException.new(
                Watir::Exception.message_for_unable_to_locate(@how, @what))
      end
    end

    def assert_enabled
      unless enabled?
        raise ObjectDisabledException, "object #{@how} and #{@what} is disabled"
      end
    end

    # return the name of the element (as defined in html)
    def_wrap_guard :name
    # return the id of the element
    def_wrap_guard :id
    # return whether the element is disabled
    def_wrap :disabled
    alias disabled? disabled
    # return the value of the element
    def_wrap_guard :value
    # return the title of the element
    def_wrap_guard :title

    def_wrap_guard :currentstyle
    # return current style instead of the inline style of the element
    alias style currentstyle

    def_wrap_guard :alt
    def_wrap_guard :src

    # return the type of the element
    def_wrap_guard :type # input elements only
    # return the url the link points to
    def_wrap :href # link only
    # return the ID of the control that this label is associated with
    def_wrap :for, :htmlFor # label only
    # return the class name of the element
    # raise an ObjectNotFound exception if the object cannot be found
    def_wrap :class_name, :className
    # return the unique COM number for the element
    def_wrap :unique_number, :uniqueNumber
    # Return the outer html of the object - see http://msdn.microsoft.com/workshop/author/dhtml/reference/properties/outerhtml.asp?frame=true
    def_wrap :html, :outerHTML

    # return the text before the element
    def before_text # label only
      assert_exists
      begin
        ole_object.getAdjacentText("afterEnd").strip
      rescue
        ''
      end
    end

    # return the text after the element
    def after_text # label only
      assert_exists
      begin
        ole_object.getAdjacentText("beforeBegin").strip
      rescue
        ''
      end
    end

    # Return the innerText of the object
    # Raise an ObjectNotFound exception if the object cannot be found
    def text
      assert_exists
      return ole_object.innerText.strip
    end

    def ole_inner_elements
      assert_exists
      return ole_object.all
    end

    private :ole_inner_elements

    def document
      assert_exists
      return ole_object
    end

    # Return the element immediately containing self. 
    def parent
      assert_exists
      result = Element.new(ole_object.parentelement)
      result.set_container self
      result
    end

    include Comparable

    def <=> other
      assert_exists
      other.assert_exists
      ole_object.sourceindex <=> other.ole_object.sourceindex
    end

    # Return true if self is contained earlier in the html than other. 
    alias :before? :<
    # Return true if self is contained later in the html than other. 
    alias :after? :>

    def typingspeed
      @container.typingspeed
    end

    def type_keys
      return @container.type_keys if @type_keys.nil?
      @type_keys
    end

    def activeObjectHighLightColor
      @container.activeObjectHighLightColor
    end

    # Return an array with many of the properties, in a format to be used by the to_s method
    def string_creator
      n = []
      n <<   "type:".ljust(TO_S_SIZE) + self.type
      n <<   "id:".ljust(TO_S_SIZE) +         self.id.to_s
      n <<   "name:".ljust(TO_S_SIZE) +       self.name.to_s
      n <<   "value:".ljust(TO_S_SIZE) +      self.value.to_s
      n <<   "disabled:".ljust(TO_S_SIZE) +   self.disabled.to_s
      return n
    end

    private :string_creator

    # Display basic details about the object. Sample output for a button is shown.
    # Raises UnknownObjectException if the object is not found.
    #      name      b4
    #      type      button
    #      id         b5
    #      value      Disabled Button
    #      disabled   true
    def to_s
      assert_exists
      return string_creator.join("\n")
    end

    # This method is responsible for setting and clearing the colored highlighting on the currently active element.
    # use :set   to set the highlight
    #   :clear  to clear the highlight
    # TODO: Make this two methods: set_highlight & clear_highlight
    # TODO: Remove begin/rescue blocks
    def highlight(set_or_clear)
      if set_or_clear == :set
        begin
          @original_color ||= style.backgroundColor
          style.backgroundColor = @container.activeObjectHighLightColor
        rescue
          @original_color = nil
        end
      else # BUG: assumes is :clear, but could actually be anything
        begin
          style.backgroundColor = @original_color unless @original_color == nil
        rescue
          # we could be here for a number of reasons...
          # e.g. page may have reloaded and the reference is no longer valid
        ensure
          @original_color = nil
        end
      end
    end

    private :highlight

    #   This method clicks the active element.
    #   raises: UnknownObjectException  if the object is not found
    #   ObjectDisabledException if the object is currently disabled
    def click
      click!
      @container.wait
    end

    def click_no_wait
      assert_exists
      assert_enabled
      highlight(:set)
      element = "#{self.class}.new(#{@page_container.attach_command}, :unique_number, #{self.unique_number})"
      # fill the $LOAD_PATH in spawned process
      ruby_code = "$:.unshift(#{$LOAD_PATH.grep(%r{watir(-.*?)?/lib}).map {|p| "'#{p}'" }.join(").unshift(")});" <<
                    "require '#{File.expand_path(File.dirname(__FILE__))}/core';#{element}.click!"
      system(spawned_click_no_wait_command(ruby_code))
      highlight(:clear)
    end

    def spawned_click_no_wait_command(command)
      command = "-e #{command.inspect}"
      unless $DEBUG
        "start rubyw #{command}"
      else
        puts "#click_no_wait command:"
        command = "ruby #{command}"
        puts command
        command
      end
    end

    private :spawned_click_no_wait_command

    def click!
      assert_exists
      assert_enabled

      highlight(:set)
      ole_object.click
      highlight(:clear)
    end

    # Flash the element the specified number of times.
    # Defaults to 10 flashes.
    def flash number=10
      assert_exists
      number.times do
        highlight(:set)
        sleep 0.05
        highlight(:clear)
        sleep 0.05
      end
      nil
    end

    # Executes a user defined "fireEvent" for objects with JavaScript events tied to them such as DHTML menus.
    #   usage: allows a generic way to fire javascript events on page objects such as "onMouseOver", "onClick", etc.
    #   raises: UnknownObjectException  if the object is not found
    #           ObjectDisabledException if the object is currently disabled
    def fire_event(event)
      assert_exists
      assert_enabled

      highlight(:set)
      ole_object.fireEvent(event)
      @container.wait
      highlight(:clear)
    end

    # This method sets focus on the active element.
    #   raises: UnknownObjectException  if the object is not found
    #           ObjectDisabledException if the object is currently disabled
    def focus
      assert_exists
      assert_enabled
      ole_object.focus
    end

    # Returns whether this element actually exists.
    def exists?
      begin
        locate if defined?(locate)
      rescue WIN32OLERuntimeError
        @o = nil
      end
      @o ? true: false
    end

    alias :exist? :exists?

    # Returns true if the element is enabled, false if it isn't.
    #   raises: UnknownObjectException  if the object is not found
    def enabled?
      assert_exists
      return ! disabled
    end

    # If any parent element isn't visible then we cannot write to the
    # element. The only realiable way to determine this is to iterate
    # up the DOM element tree checking every element to make sure it's
    # visible.
    def visible?
      # Now iterate up the DOM element tree and return false if any
      # parent element isn't visible 
      assert_exists
      object = @o
      while object
        begin
          if object.currentstyle.invoke('visibility') =~ /^hidden$/i
            return false
          end
          if object.currentstyle.invoke('display') =~ /^none$/i
            return false
          end
        rescue WIN32OLERuntimeError
        end
        object = object.parentElement
      end
      true
    end

    # Get attribute value for any attribute of the element.
    # Returns null if attribute doesn't exist.
    def attribute_value(attribute_name)
      assert_exists
      return ole_object.getAttribute(attribute_name)
    end

  end

  class ElementMapper # Still to be used
    include Container

    def initialize wrapper_class, container, how, what
      @wrapper_class = wrapper_class
      set_container
      @how = how
      @what = what
    end

    def method_missing method, *args
      locate
      @wrapper_class.new(@o).send(method, *args)
    end
  end
end