require 'vapir-ie/container'
require 'vapir-common/page_container'

module Vapir
  # A PageContainer contains an HTML Document. In other words, it is a 
  # what JavaScript calls a Window.
  #
  # this assumes that document_object is defined on the includer. 
  module IE::PageContainer
    include Vapir::PageContainer

    # Used internally to determine when IE has finished loading a page
    # http://msdn.microsoft.com/en-us/library/system.windows.forms.webbrowserreadystate.aspx
    # http://msdn.microsoft.com/en-us/library/system.windows.forms.webbrowser.readystate.aspx
    module WebBrowserReadyState
      Uninitialized = 0 # No document is currently loaded.
      Loading       = 1 # The control is loading a new document.
      Loaded        = 2 # The control has loaded and initialized the new document, but has not yet received all the document data.
      Interactive   = 3 # The control has loaded enough of the document to allow limited user interaction, such as clicking hyperlinks that have been displayed.
      Complete      = 4 # The control has finished loading the new document and all its contents.
    end
    READYSTATE_COMPLETE = WebBrowserReadyState::Complete
       
    include IE::Container
    include Vapir::Exception

    # This method checks the currently displayed page for http errors, 404, 500 etc
    # It gets called internally by the wait method, so a user does not need to call it explicitly
    def check_for_http_error
      # check for IE7
      n = self.document.invoke('parentWindow').navigator.appVersion
      m=/MSIE\s(.*?);/.match( n )
      if m and m[1] =='7.0'
        if m = /HTTP (\d\d\d.*)/.match( self.title )
          raise NavigationException, m[1]
        end
      else
        # assume its IE6
        url = self.document.location.href
        if /shdoclc.dll/.match(url)
          m = /id=IEText.*?>(.*?)</i.match(self.html)
          raise NavigationException, m[1] if m
        end
      end
      false
    end 
    
    def content_window_object
      document_object.parentWindow
    end
    
    # The HTML of the current page
    def html
      document_element.outerHTML
    end
    
    # The text of the current page
    def text
      document_element.innerText
    end

    def close
      content_window_object.close
    end

    # Execute the given JavaScript string
    def execute_script(source)
      retried=false
      result=nil
      begin
        result=document_object.parentWindow.eval(source)
      rescue WIN32OLERuntimeError, NoMethodError
        # don't retry more than once; don't catch anything but the particular thing we're looking for 
        if retried || $!.message !~ /unknown property or method:? `eval'/
          raise
        end
        # this can happen if no scripts have executed at all - the 'eval' function doesn't exist. 
        # execScript works, but behaves differently than eval (it doesn't return anything) - but 
        # once an execScript has run, eval is subsequently defined. so, execScript a blank script, 
        # and then try again with eval.
        document_object.parentWindow.execScript('null')
        retried=true
        retry
      end
      return result
    end
    
    # Block execution until the page has loaded.
    # =nodoc
    # Note: This code needs to be prepared for the ie object to be closed at 
    # any moment!
    def wait(options={})
      return unless config.wait
      unless options.is_a?(Hash)
        raise ArgumentError, "given options should be a Hash, not #{options.inspect} (#{options.class})\nold conflicting arguments of no_sleep or last_url are gone"
      end
      options={:sleep => false, :interval => 0.1, :timeout => config.wait_timeout}.merge(options)
      @xml_parser_doc = nil
      @down_load_time = nil
      start_load_time = Time.now
      
      if respond_to?(:browser_object)
        ::Waiter.try_for(options[:timeout]-(Time.now-start_load_time), :interval => options[:interval], :exception => "The browser was still busy after #{options[:timeout]} seconds") do
          return unless exists?
          handling_existence_failure(:handle => proc { false }) { !browser_object.busy }
        end
        ::Waiter.try_for(options[:timeout]-(Time.now-start_load_time), :interval => options[:interval], :exception => "The browser's readyState was still not ready for interaction after #{options[:timeout]} seconds") do
          return unless exists?
          handling_existence_failure(:handle => proc { false }) do
            [WebBrowserReadyState::Interactive, WebBrowserReadyState::Complete].include?(browser_object.readyState)
          end
        end
      end
      # if the document object is gone, then we want to just return. 
      # in subsequent code where we want the document object, we will call this proc 
      # so that we don't have to deal with checking for error / returning every time. 
      doc_or_ret = proc do
        begin
          document_object
        rescue WIN32OLERuntimeError, NoMethodError
          return
        end
      end
      ::Waiter.try_for(options[:timeout]-(Time.now-start_load_time), :interval => options[:interval], :exception => "The browser's document was still not defined after #{options[:timeout]} seconds") do
        return unless exists?
        doc_or_ret.call
      end
      urls=[]
      all_frames_complete_result=::Waiter.try_for(options[:timeout]-(Time.now-start_load_time), :interval => options[:interval], :exception => nil, :condition => proc{|result| result==true }) do
        return unless exists?
        all_frames_complete?(doc_or_ret.call, urls)
      end
      case all_frames_complete_result
      when false
        raise "A frame on the browser did not come into readyState complete after #{options[:timeout]} seconds"
      when ::Exception
        message = "A frame on the browser encountered an error.\n"
        if all_frames_complete_result.message =~ /0x80070005/
          message += "An 'Access is denied' error might be fixed by adding the domain of the site to your 'Trusted Sites'.\n"
        end
        message+="Original message was:\n\n"
        message+=all_frames_complete_result.message
        raise all_frames_complete_result.class, message, all_frames_complete_result.backtrace
      when true
        # dandy; carry on. 
      else
        # this should never happen. 
        raise "Unexpected result from all_frames_complete?: #{all_frames_complete_result.inspect}"
      end
      @url_list=(@url_list || [])+urls
      
      @down_load_time= Time.now - start_load_time
      run_error_checks if respond_to?(:run_error_checks)
      sleep @pause_after_wait if options[:sleep]
      @down_load_time
    end
    alias page_container_wait wait # alias this so that Frame can clobber the #wait method 
    
    private
    # this returns true if all frames are complete. 
    # it returns false if a frame is incomplete. 
    # if an unexpected exception is encountered, it returns that exception. yes, returns, not raises, 
    # due to the fact that an exception may indicate either the frame not being complete, or an actual
    # error condition - it is difficult to differentiate. in the usage above, in #wait, we check
    # if an exception is still being raised at the end of the specified interval, and raise it if so. 
    # if it stops being raised, we carry on. 
    def all_frames_complete?(document, urls=nil)
      begin
        if urls && !urls.include?(document.location.href)
          urls << document.location.href
        end
        frames=document.frames
        return ['complete', 'interactive'].include?(document.readyState) && (0...frames.length).all? do |i|
          frame=document.frames[i.to_s]
          frame_document=begin
            frame.document
          rescue WIN32OLERuntimeError, NoMethodError
            $!
          end
          case frame_document
          when nil
            # frame hasn't loaded to the point where it has a document yet 
            false
          when WIN32OLE
            # frame has a document - check recursively 
            all_frames_complete?(frame_document, urls)
          when WIN32OLERuntimeError, NoMethodError
            # if we get a WIN32OLERuntimeError with access denied, that is probably a 404 and it's not going 
            # to load, so no reason to keep waiting for it - consider it 'complete' and return true. 
            # there's probably a better method of determining this but I haven't found it yet. 
            true
          else # don't know what we'd have here 
            raise RuntimeError, "unknown frame.document: #{frame_document.inspect} (#{frame_document.class})"
          end
        end
      rescue WIN32OLERuntimeError, NoMethodError
        return $!
      end
    end
  end # module
end