// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "Browser.h"

#include <comutil.h>
#include <ShlGuid.h>

#include "errorcodes.h"
#include "logging.h"

#include "Alert.h"
#include "BrowserFactory.h"
#include "CustomTypes.h"
#include "messages.h"
#include "HookProcessor.h"
#include "Script.h"
#include "StringUtilities.h"
#include "WebDriverConstants.h"
#include "WindowUtilities.h"

namespace webdriver {

Browser::Browser(IWebBrowser2* browser, HWND hwnd, HWND session_handle, bool is_edge_chromium) : DocumentHost(hwnd, session_handle) {
  LOG(TRACE) << "Entering Browser::Browser";
  this->is_explicit_close_requested_ = false;
  this->is_navigation_started_ = false;
  this->browser_ = browser;
  this->AttachEvents();
  this->set_is_edge_chromium(is_edge_chromium);
}

Browser::~Browser(void) {
  this->DetachEvents();
}

void __stdcall Browser::BeforeNavigate2(IDispatch* pObject,
                                        VARIANT* pvarUrl,
                                        VARIANT* pvarFlags,
                                        VARIANT* pvarTargetFrame,
                                        VARIANT* pvarData,
                                        VARIANT* pvarHeaders,
                                        VARIANT_BOOL* pbCancel) {
  LOG(TRACE) << "Entering Browser::BeforeNavigate2";
  std::wstring url(pvarUrl->bstrVal);

  LOG(DEBUG) << "BeforeNavigate2: Url: " << LOGWSTRING(url) << ", TargetFrame: " << pvarTargetFrame->bstrVal;
}

void __stdcall Browser::OnQuit() {
  LOG(TRACE) << "Entering Browser::OnQuit";
  if (!this->is_explicit_close_requested_) {
    if (this->is_awaiting_new_process()) {
      LOG(WARN) << "A new browser process was requested. This means a Protected "
                << "Mode boundary has been crossed, and that future commands to "
                << "the current browser instance will fail. The driver will "
                << "attempt to reconnect to the newly created browser object, "
                << "but there is no guarantee it will work.";
      DWORD process_id;
      HWND window_handle = this->GetBrowserWindowHandle();
      ::GetWindowThreadProcessId(window_handle, &process_id);

      BrowserReattachInfo* info = new BrowserReattachInfo;
      info->browser_id = this->browser_id();
      info->current_process_id = process_id;
      info->known_process_ids = this->known_process_ids_;

      this->DetachEvents();
      this->browser_ = NULL;
      ::PostMessage(this->executor_handle(),
                    WD_BROWSER_REATTACH,
                    NULL,
                    reinterpret_cast<LPARAM>(info));
      return;
    } else {
      LOG(WARN) << "This instance of Internet Explorer (" << this->browser_id()
                << ") is exiting without an explicit request to close it. "
                << "Unless you clicked a link that specifically attempts to "
                << "close the page, that likely means a Protected Mode "
                << "boundary has been crossed (either entering or exiting "
                << "Protected Mode). It is highly likely that any subsequent "
                << "commands to this driver instance will fail. THIS IS NOT A "
                << "BUG IN THE IE DRIVER! Fix your code and/or browser "
                << "configuration so that a Protected Mode boundary is not "
                << "crossed.";
    }
  }
  this->PostQuitMessage();
}

void __stdcall Browser::NewProcess(DWORD lCauseFlag,
                                   IDispatch* pWB2,
                                   VARIANT_BOOL* pbCancel) {
  LOG(TRACE) << "Entering Browser::NewProcess";
  this->InitiateBrowserReattach();
}

void __stdcall Browser::NewWindow3(IDispatch** ppDisp,
                                   VARIANT_BOOL* pbCancel,
                                   DWORD dwFlags,
                                   BSTR bstrUrlContext,
                                   BSTR bstrUrl) {
  LOG(TRACE) << "Entering Browser::NewWindow3";
  ::PostMessage(this->executor_handle(), WD_BEFORE_NEW_WINDOW, NULL, NULL);
  std::vector<HWND>* ie_window_handles = nullptr;
  WPARAM param_flag = 0;

  if (this->is_edge_chromium()) {
    param_flag = 1000;
    //HWND top_level_handle = this->GetTopLevelWindowHandle();
    // 1) find all Edge browser window handles
    std::vector<HWND>edge_window_handles;
    ::EnumWindows(&BrowserFactory::FindEdgeBrowserHandles,
                  reinterpret_cast<LPARAM>(&edge_window_handles));

    // 2) find all IE browser window handlers as child window when Edge runs in IEMode
    ie_window_handles = new std::vector<HWND>;
    for (HWND& edge_window_handle : edge_window_handles) {
      std::vector<HWND> child_window_handles;
      ::EnumChildWindows(edge_window_handle,
                         &BrowserFactory::FindIEBrowserHandles,
                         reinterpret_cast<LPARAM>(&child_window_handles));

      for (HWND& child_window_handle : child_window_handles) {
        ie_window_handles->push_back(child_window_handle);
      }
    }
  } else {
    // Handle the NewWindow3 event to allow us to immediately hook
    // the events of the new browser window opened by the user action.
    // The three ways we can respond to this event are documented at
    // http://msdn.microsoft.com/en-us/library/aa768337%28v=vs.85%29.aspx
    // We potentially use two of those response methods.
    // This will not allow us to handle windows created by the JavaScript
    // showModalDialog function().
    std::wstring url = bstrUrl;
    IWebBrowser2* browser;
    NewWindowInfo info;
    info.target_url = StringUtilities::ToString(url);
    LRESULT create_result = ::SendMessage(this->executor_handle(),
                                          WD_BROWSER_NEW_WINDOW,
                                          NULL,
                                          reinterpret_cast<LPARAM>(&info));
    if (create_result != 0) {
      // The new, blank IWebBrowser2 object was not created,
      // so we can't really do anything appropriate here.
      // Note this is "response method 2" of the aforementioned
      // documentation.
      LOG(WARN) << "A valid IWebBrowser2 object could not be created.";
      *pbCancel = VARIANT_TRUE;
      ::PostMessage(this->executor_handle(), WD_AFTER_NEW_WINDOW, NULL, NULL);
      return;
    }

    // We received a valid IWebBrowser2 pointer, so deserialize it onto this
    // thread, and pass the result back to the caller.
    HRESULT hr = ::CoGetInterfaceAndReleaseStream(info.browser_stream,
                                                  IID_IWebBrowser2,
                                                  reinterpret_cast<void**>(&browser));
    if (FAILED(hr)) {
      LOGHR(WARN, hr) << "Failed to marshal IWebBrowser2 interface from stream.";
    }

    *ppDisp = browser;
  }

  // 3) pass all IE window handles to WD_AFTER_NEW_WINDOW
  ::PostMessage(this->executor_handle(),
                WD_AFTER_NEW_WINDOW,
                param_flag,
                reinterpret_cast<LPARAM>(ie_window_handles));
}

void __stdcall Browser::DocumentComplete(IDispatch* pDisp, VARIANT* URL) {
  LOG(TRACE) << "Entering Browser::DocumentComplete";

  // Flag the browser as navigation having started.
  this->is_navigation_started_ = true;

  // DocumentComplete fires last for the top-level frame. If it fires
  // for the top-level frame and the focused_frame_window_ member variable
  // is not NULL, we assume we have navigated from within a frameset to a
  // link that has a target of "_top", which replaces the frameset with the
  // target page. On a top-level navigation, we are supposed to reset the
  // focused frame to the top-level, so we do that here.
  // NOTE: This is a possible source of unreliability if the above 
  // assumptions turn out to be wrong and/or the event firing doesn't work
  // the way we expect it to.
  CComPtr<IDispatch> dispatch(this->browser_);
  if (dispatch.IsEqualObject(pDisp)) {
    if (this->focused_frame_window() != NULL) {
      LOG(DEBUG) << "DocumentComplete happened from within a frameset";
      this->SetFocusedFrameByElement(NULL);
    }
  }
}

void Browser::InitiateBrowserReattach() {
  LOG(TRACE) << "Entering Browser::InitiateBrowserReattach";
  LOG(DEBUG) << "Requesting browser reattach";
  this->known_process_ids_.clear();
  WindowUtilities::GetProcessesByName(L"iexplore.exe",
                                      &this->known_process_ids_);
  this->set_is_awaiting_new_process(true);
  ::SendMessage(this->executor_handle(), WD_BEFORE_BROWSER_REATTACH, NULL, NULL);
}

void Browser::ReattachBrowser(IWebBrowser2* browser) {
  LOG(TRACE) << "Entering Browser::ReattachBrowser";
  this->browser_ = browser;
  this->AttachEvents();
  this->set_is_awaiting_new_process(false);
  LOG(DEBUG) << "Reattach complete";
}

void Browser::GetDocument(IHTMLDocument2** doc) {
  this->GetDocument(false, doc);
}

void Browser::GetDocument(const bool force_top_level_document,
                          IHTMLDocument2** doc) {
  LOG(TRACE) << "Entering Browser::GetDocument";
  CComPtr<IHTMLWindow2> window;

  if (this->focused_frame_window() == NULL || force_top_level_document) {
    LOG(INFO) << "No child frame focus. Focus is on top-level frame";

    CComPtr<IDispatch> dispatch;
    HRESULT hr = this->browser_->get_Document(&dispatch);
    if (FAILED(hr)) {
      LOGHR(WARN, hr) << "Unable to get document, IWebBrowser2::get_Document call failed";
      return;
    }

    CComPtr<IHTMLDocument2> dispatch_doc;
    hr = dispatch->QueryInterface(&dispatch_doc);
    if (FAILED(hr)) {
      LOGHR(WARN, hr) << "Have document but cannot cast, IDispatch::QueryInterface call failed";
      return;
    }

    dispatch_doc->get_parentWindow(&window);
  } else {
    window = this->focused_frame_window();
  }

  if (window) {
    bool result = this->GetDocumentFromWindow(window, doc);
    if (!result) {
      LOG(WARN) << "Cannot get document";
    }
  } else {
    LOG(WARN) << "No window is found";
  }
}

std::string Browser::GetTitle() {
  LOG(TRACE) << "Entering Browser::GetTitle";

  CComPtr<IDispatch> dispatch;
  HRESULT hr = this->browser_->get_Document(&dispatch);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Unable to get document, IWebBrowser2::get_Document call failed";
    return "";
  }

  CComPtr<IHTMLDocument2> doc;
  hr = dispatch->QueryInterface(&doc);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Have document but cannot cast, IDispatch::QueryInterface call failed";
    return "";
  }

  CComBSTR title;
  hr = doc->get_title(&title);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Unable to get document title, call to IHTMLDocument2::get_title failed";
    return "";
  }

  std::wstring converted_title = title;
  std::string title_string = StringUtilities::ToString(converted_title);
  return title_string;
}

std::string Browser::GetBrowserUrl() {
  LOG(TRACE) << "Entering Browser::GetBrowserUrl";

  CComBSTR url;
  HRESULT hr = this->browser_->get_LocationURL(&url);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Unable to get current URL, call to IWebBrowser2::get_LocationURL failed";
    return "";
  }

  std::wstring converted_url = url;
  std::string current_url = StringUtilities::ToString(converted_url);
  return current_url;
}

HWND Browser::GetContentWindowHandle() {
  LOG(TRACE) << "Entering Browser::GetContentWindowHandle";

  HWND current_content_window_handle = this->window_handle();
  // If this window is closing, the only reason to care about
  // a valid window handle is to check for alerts whose parent
  // is this window handle, so return the stored window handle.
  if (!this->is_closing()) {
    // If, for some reason, the window handle is no longer valid, set the
    // member variable to NULL so that we can reacquire the valid window
    // handle. Note that this can happen when browsing from one type of
    // content to another, like from HTML to a transformed XML page that
    // renders content. If the member variable is NULL upon entering this
    // method, that is okay, as it typically means only that this object
    // is newly constructed, and has not yet had its handle set.
    bool window_handle_is_valid = ::IsWindow(current_content_window_handle);
    if (!window_handle_is_valid) {
      LOG(INFO) << "Flushing window handle as it is no longer valid";
      this->set_window_handle(NULL);
    }

    if (this->window_handle() == NULL) {
      LOG(INFO) << "Restore window handle from tab";
      // GetBrowserWindowHandle gets the TabWindowClass window in IE 7 and 8,
      // and the top-level window frame in IE 6. The window we need is the
      // InternetExplorer_Server window.
      HWND tab_window_handle = this->GetBrowserWindowHandle();
      if (tab_window_handle == NULL) {
        LOG(WARN) << "No tab window found";
      }
      HWND content_window_handle = this->FindContentWindowHandle(tab_window_handle);
      this->set_window_handle(content_window_handle);
    }
  }

  return this->window_handle();
}

std::string Browser::GetWindowName() {
  LOG(TRACE) << "Entering Browser::GetWindowName";

  CComPtr<IDispatch> dispatch;
  HRESULT hr = this->browser_->get_Document(&dispatch);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Unable to get document, IWebBrowser2::get_Document call failed";
    return "";
  }

  CComPtr<IHTMLDocument2> doc;
  dispatch->QueryInterface<IHTMLDocument2>(&doc);
  if (!doc) {
    LOGHR(WARN, hr) << "Have document but cannot cast, IDispatch::QueryInterface call failed";
    return "";
  }

  CComPtr<IHTMLWindow2> window;
  hr = doc->get_parentWindow(&window);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Unable to get parent window, call to IHTMLDocument2::get_parentWindow failed";
    return "";
  }

  std::string name = "";
  CComBSTR window_name;
  hr = window->get_name(&window_name);
  if (window_name) {
    std::wstring converted_window_name = window_name;
    name = StringUtilities::ToString(converted_window_name);
  } else {
    LOG(WARN) << "Unable to get window name, IHTMLWindow2::get_name failed or returned a NULL value";
  }

  return name;
}

long Browser::GetWidth() {
  LOG(TRACE) << "Entering Browser::GetWidth";
  long width = 0;
  this->browser_->get_Width(&width);
  return width;
}

long Browser::GetHeight() {
  LOG(TRACE) << "Entering Browser::GetHeight";
  long height = 0;
  this->browser_->get_Height(&height);
  return height;
}

void Browser::SetWidth(long width) {
  LOG(TRACE) << "Entering Browser::SetWidth";
  this->browser_->put_Width(width);
}

void Browser::SetHeight(long height) {
  LOG(TRACE) << "Entering Browser::SetHeight";
  this->browser_->put_Height(height);
}

void Browser::AttachEvents() {
  LOG(TRACE) << "Entering Browser::AttachEvents";
  CComPtr<IUnknown> unknown;
  this->browser_->QueryInterface<IUnknown>(&unknown);
  HRESULT hr = this->DispEventAdvise(unknown);
}

void Browser::DetachEvents() {
  LOG(TRACE) << "Entering Browser::DetachEvents";
  CComPtr<IUnknown> unknown;
  this->browser_->QueryInterface<IUnknown>(&unknown);
  HRESULT hr = this->DispEventUnadvise(unknown);
}

void Browser::Close() {
  LOG(TRACE) << "Entering Browser::Close";
  if (this->is_edge_chromium()) {
    // For Edge in IE Mode, cache the top-level hosting Chromium window
    // handle so they can be properly closed on quit.
    HWND top_level_window_handle = this->GetTopLevelWindowHandle();
    ::SendMessage(this->executor_handle(),
                  WD_ADD_CHROMIUM_WINDOW_HANDLE,
                  reinterpret_cast<WPARAM>(top_level_window_handle),
                  NULL);
  }

  this->is_explicit_close_requested_ = true;
  this->set_is_closing(true);
  // Closing the browser, so having focus on a frame doesn't
  // make any sense.
  this->SetFocusedFrameByElement(NULL);

  HRESULT hr = S_OK;
  hr = this->browser_->Stop();
  hr = this->browser_->Quit();

  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Call to IWebBrowser2::Quit failed";
  }
}

int Browser::NavigateToUrl(const std::string& url,
                           std::string* error_message) {
  LOG(TRACE) << "Entring Browser::NavigateToUrl";

  std::wstring wide_url = StringUtilities::ToWString(url);
  CComVariant url_variant(wide_url.c_str());
  CComVariant dummy;

  HRESULT hr = this->browser_->Navigate2(&url_variant,
                                         &dummy,
                                         &dummy,
                                         &dummy,
                                         &dummy);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Call to IWebBrowser2::Navigate2 failed";
    _com_error error(hr);
    std::wstring formatted_message = StringUtilities::Format(
        L"Received error: 0x%08x ['%s']",
        hr,
        error.ErrorMessage());
    *error_message = StringUtilities::ToString(formatted_message);
    return EUNHANDLEDERROR;
  }

  this->set_wait_required(true);
  return WD_SUCCESS;
}

int Browser::NavigateBack() {
  LOG(TRACE) << "Entering Browser::NavigateBack";
  LPSTREAM stream;
  HRESULT hr = ::CoMarshalInterThreadInterfaceInStream(IID_IWebBrowser2, this->browser_, &stream);
  unsigned int thread_id = 0;
  HANDLE thread_handle = reinterpret_cast<HANDLE>(_beginthreadex(NULL,
                                                  0,
                                                  &Browser::GoBackThreadProc,
                                                  (void *)stream,
                                                  0,
                                                  &thread_id));
  if (thread_handle != NULL) {
    ::CloseHandle(thread_handle);
  }

  this->set_wait_required(true);
  return WD_SUCCESS;
}

unsigned int WINAPI Browser::GoBackThreadProc(LPVOID param) {
  HRESULT hr = ::CoInitialize(NULL);
  IWebBrowser2* browser;
  LPSTREAM message_payload = reinterpret_cast<LPSTREAM>(param);
  hr = ::CoGetInterfaceAndReleaseStream(message_payload,
                                        IID_IWebBrowser2,
                                        reinterpret_cast<void**>(&browser));
  if (browser != NULL) {
    hr = browser->GoBack();
  }
  return 0;
}

int Browser::NavigateForward() {
  LOG(TRACE) << "Entering Browser::NavigateForward";
  LPSTREAM stream;
  HRESULT hr = ::CoMarshalInterThreadInterfaceInStream(IID_IWebBrowser2, this->browser_, &stream);
  unsigned int thread_id = 0;
  HANDLE thread_handle = reinterpret_cast<HANDLE>(_beginthreadex(NULL,
                                                  0,
                                                  &Browser::GoForwardThreadProc,
                                                  (void *)stream,
                                                  0,
                                                  &thread_id));
  if (thread_handle != NULL) {
    ::CloseHandle(thread_handle);
  }

  this->set_wait_required(true);
  return WD_SUCCESS;
}

unsigned int WINAPI Browser::GoForwardThreadProc(LPVOID param) {
  HRESULT hr = ::CoInitialize(NULL);
  IWebBrowser2* browser;
  LPSTREAM message_payload = reinterpret_cast<LPSTREAM>(param);
  hr = ::CoGetInterfaceAndReleaseStream(message_payload,
                                        IID_IWebBrowser2,
                                        reinterpret_cast<void**>(&browser));
  if (browser != NULL) {
    hr = browser->GoForward();
  }
  return 0;
}

int Browser::Refresh() {
  LOG(TRACE) << "Entering Browser::Refresh";

  HRESULT hr = this->browser_->Refresh();
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Call to IWebBrowser2::Refresh failed";
  }

  this->set_wait_required(true);
  return WD_SUCCESS;
}

HWND Browser::GetTopLevelWindowHandle() {
  LOG(TRACE) << "Entering Browser::GetTopLevelWindowHandle";

  HWND top_level_window_handle = NULL;
  HRESULT hr = this->browser_->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&top_level_window_handle));
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Getting HWND property of IWebBrowser2 object failed";
  }

  return top_level_window_handle;
}

bool Browser::IsValidWindow() {
  LOG(TRACE) << "Entering Browser::IsValidWindow";
  // This is a no-op for this class. Full browser windows can properly notify
  // of their window's validity by using the proper events.
  return true;
}

bool Browser::IsBusy() {
  VARIANT_BOOL is_busy(VARIANT_FALSE);
  HRESULT hr = this->browser_->get_Busy(&is_busy);
  return SUCCEEDED(hr) && is_busy == VARIANT_TRUE;
}

bool Browser::Wait(const std::string& page_load_strategy) {
  LOG(TRACE) << "Entering Browser::Wait";

  if (page_load_strategy == NONE_PAGE_LOAD_STRATEGY) {
    LOG(DEBUG) << "Page load strategy is 'none'. Aborting wait.";
    this->set_wait_required(false);
    return true;
  }

  if (this->is_awaiting_new_process()) {
    return false;
  }

  bool is_navigating = true;

  LOG(DEBUG) << "Navigate Events Completed.";
  this->is_navigation_started_ = false;

  HWND dialog = this->GetActiveDialogWindowHandle();
  if (dialog != NULL) {
    LOG(DEBUG) << "Found alert. Aborting wait.";
    this->set_wait_required(false);
    return true;
  }

  // Navigate events completed. Waiting for browser.Busy != false...
  is_navigating = this->is_navigation_started_;
  if (is_navigating || (page_load_strategy == NORMAL_PAGE_LOAD_STRATEGY && this->IsBusy())) {
    if (is_navigating) {
      LOG(DEBUG) << "DocumentComplete event fired, indicating a new navigation.";
    } else {
      LOG(DEBUG) << "Browser busy property is true.";
    }
    return false;
  }

  READYSTATE expected_ready_state = READYSTATE_COMPLETE;
  if (page_load_strategy == EAGER_PAGE_LOAD_STRATEGY) {
    expected_ready_state = READYSTATE_INTERACTIVE;
  }

  // Waiting for browser.ReadyState >= expected ready state
  is_navigating = this->is_navigation_started_;
  READYSTATE ready_state;
  HRESULT hr = this->browser_->get_ReadyState(&ready_state);
  if (is_navigating || FAILED(hr) || ready_state < expected_ready_state) {
    if (is_navigating) {
      LOG(DEBUG) << "DocumentComplete event fired, indicating a new navigation.";
    } else if (FAILED(hr)) {
      LOGHR(DEBUG, hr) << "IWebBrowser2::get_ReadyState failed.";
    } else {
      LOG(DEBUG) << "Browser ReadyState is not at least '" << expected_ready_state << "'; it was " << ready_state;
    }
    return false;
  }

  // Waiting for document property != null...
  is_navigating = this->is_navigation_started_;
  CComPtr<IDispatch> document_dispatch;
  hr = this->browser_->get_Document(&document_dispatch);
  if (is_navigating || FAILED(hr) || !document_dispatch) {
    if (is_navigating) {
      LOG(DEBUG) << "DocumentComplete event fired, indicating a new navigation.";
    } else if (FAILED(hr)) {
      LOGHR(DEBUG, hr) << "IWebBrowser2::get_Document failed.";
    } else {
      LOG(DEBUG) << "Get Document failed; IWebBrowser2::get_Document did not return a valid IDispatch object.";
    }
    return false;
  }

  // Waiting for document to complete...
  CComPtr<IHTMLDocument2> doc;
  hr = document_dispatch->QueryInterface(&doc);
  if (SUCCEEDED(hr)) {
    LOG(DEBUG) << "Waiting for document to complete...";
    is_navigating = this->IsDocumentNavigating(page_load_strategy, doc);
  }

  if (!is_navigating) {
    LOG(DEBUG) << "Not in navigating state";
    this->set_wait_required(false);
  }

  return !is_navigating;
}

bool Browser::IsDocumentNavigating(const std::string& page_load_strategy,
                                   IHTMLDocument2* doc) {
  LOG(TRACE) << "Entering Browser::IsDocumentNavigating";

  bool is_navigating = true;

  // Starting WaitForDocumentComplete()
  is_navigating = this->is_navigation_started_;
  CComBSTR ready_state_bstr;
  HRESULT hr = doc->get_readyState(&ready_state_bstr);
  if (FAILED(hr) || is_navigating) {
    if (FAILED(hr)) {
      LOGHR(DEBUG, hr) << "IHTMLDocument2::get_readyState failed.";
    } else if (is_navigating) {
      LOG(DEBUG) << "DocumentComplete event fired, indicating a new navigation.";
    }
    return true;
  } else {
    std::wstring ready_state = ready_state_bstr;
    if ((ready_state == L"complete") ||
        (page_load_strategy == EAGER_PAGE_LOAD_STRATEGY && ready_state == L"interactive")) {
      is_navigating = false;
    } else {
      if (page_load_strategy == EAGER_PAGE_LOAD_STRATEGY) {
        LOG(DEBUG) << "document.readyState is not 'complete' or 'interactive'; it was " << LOGWSTRING(ready_state);
      } else {
        LOG(DEBUG) << "document.readyState is not 'complete'; it was " << LOGWSTRING(ready_state);
      }
      return true;
    }
  }

  // document.readyState == complete
  is_navigating = this->is_navigation_started_;
  CComPtr<IHTMLFramesCollection2> frames;
  hr = doc->get_frames(&frames);
  if (is_navigating || FAILED(hr)) {
    LOG(DEBUG) << "Could not get frames, navigation has started or call to IHTMLDocument2::get_frames failed";
    return true;
  }

  if (frames != NULL) {
    long frame_count = 0;
    hr = frames->get_length(&frame_count);

    CComVariant index;
    index.vt = VT_I4;
    for (long i = 0; i < frame_count; ++i) {
      // Waiting on each frame
      index.lVal = i;
      CComVariant result;
      hr = frames->item(&index, &result);
      if (FAILED(hr)) {
        LOGHR(DEBUG, hr) << "Could not get frame item for index "
                         << i
                         << ", call to IHTMLFramesCollection2::item failed, frame/frameset is broken";
        continue;
      }

      CComPtr<IHTMLWindow2> window;
      result.pdispVal->QueryInterface<IHTMLWindow2>(&window);
      if (!window) {        
        LOG(DEBUG) << "Could not get window for frame item with index "
                   << i 
                   << ", cast to IHTMLWindow2 failed, frame is not an HTML frame";
        continue;
      }

      CComPtr<IHTMLDocument2> frame_document;
      bool is_valid_frame_document = this->GetDocumentFromWindow(window,
                                                                 &frame_document);

      is_navigating = this->is_navigation_started_;
      if (is_navigating) {
        break;
      }

      // Recursively call to wait for the frame document to complete
      if (is_valid_frame_document) {
        is_navigating = this->IsDocumentNavigating(page_load_strategy, frame_document);
        if (is_navigating) {
          break;
        }
      }
    }
  } else {
    LOG(DEBUG) << "IHTMLDocument2.get_frames() returned empty collection";
  }
  return is_navigating;
}

bool Browser::GetDocumentFromWindow(IHTMLWindow2* window,
                                    IHTMLDocument2** doc) {
  LOG(TRACE) << "Entering Browser::GetDocumentFromWindow";

  HRESULT hr = window->get_document(doc);
  if (SUCCEEDED(hr)) {
    return true;
  }

  if (hr == E_ACCESSDENIED) {
    // Cross-domain documents may throw Access Denied. If so,
    // get the document through the IWebBrowser2 interface.
    CComPtr<IServiceProvider> service_provider;
    hr = window->QueryInterface<IServiceProvider>(&service_provider);
    if (FAILED(hr)) {
      LOGHR(WARN, hr) << "Unable to get browser, call to IHTMLWindow2::QueryService failed for IServiceProvider";
      return false;
    }

    CComPtr<IWebBrowser2> window_browser;
    hr = service_provider->QueryService(IID_IWebBrowserApp, &window_browser);
    if (FAILED(hr)) {
      LOGHR(WARN, hr) << "Unable to get browser, call to IServiceProvider::QueryService failed for IID_IWebBrowserApp";
      return false;
    }

    CComPtr<IDispatch> document_dispatch;
    hr = window_browser->get_Document(&document_dispatch);
    if (FAILED(hr) || hr == S_FALSE) {
      LOGHR(WARN, hr) << "Unable to get document, call to IWebBrowser2::get_Document failed";
      return false;
    }

    hr = document_dispatch->QueryInterface(doc);
    if (FAILED(hr)) {
      LOGHR(WARN, hr) << "Unable to query document, call to IDispatch::QueryInterface failed.";
      return false;
    }

    return true;
  } else {
    LOGHR(WARN, hr) << "Unable to get main document, IHTMLWindow2::get_document returned other than E_ACCESSDENIED";
  }

  return false;
}

HWND Browser::GetBrowserWindowHandle() {
  LOG(TRACE) << "Entering Browser::GetBrowserWindowHandle";

  HWND hwnd = NULL;
  CComPtr<IServiceProvider> service_provider;
  HRESULT hr = this->browser_->QueryInterface(IID_IServiceProvider,
                                              reinterpret_cast<void**>(&service_provider));
  HWND hwnd_tmp = NULL;
  this->browser_->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&hwnd_tmp));
  if (SUCCEEDED(hr)) {
    CComPtr<IOleWindow> window;
    hr = service_provider->QueryService(SID_SShellBrowser,
                                        IID_IOleWindow,
                                        reinterpret_cast<void**>(&window));
    if (SUCCEEDED(hr)) {
      // This gets the TabWindowClass window in IE 7 and 8,
      // and the top-level window frame in IE 6.
      window->GetWindow(&hwnd);
    } else {
      LOGHR(WARN, hr) << "Unable to get window, call to IOleWindow::QueryService for SID_SShellBrowser failed";
    }
  } else {
    LOGHR(WARN, hr) << "Unable to get service, call to IWebBrowser2::QueryInterface for IID_IServiceProvider failed";
  }

  return hwnd;
}

bool Browser::SetFullScreen(bool is_full_screen) {
  VARIANT_BOOL full_screen_value = VARIANT_TRUE;
  std::wstring full_screen_script = L"window.fullScreen = true;";
  if (!is_full_screen) {
    full_screen_value = VARIANT_FALSE;
    full_screen_script = L"delete window.fullScreen;";
  }
  this->browser_->put_FullScreen(full_screen_value);

  // IE does not support the W3C Fullscreen API (and likely never will).
  // The prefixed version cannot be triggered via JavaScript outside of
  // a user interaction, so we're going to cheat here and manually set
  // the fullScreen property of the window object to the appropriate
  // value. This may interfere with polyfills in use, and if that's
  // the case, we'll revisit this hack.
  CComPtr<IHTMLDocument2> doc;
  this->GetDocument(true, &doc);
  std::wstring script = ANONYMOUS_FUNCTION_START;
  script += full_screen_script;
  script += ANONYMOUS_FUNCTION_END;
  Script script_wrapper(doc, script, 0);
  script_wrapper.Execute();
  return true;
}

bool Browser::IsFullScreen() {
  VARIANT_BOOL is_full_screen = VARIANT_FALSE;
  this->browser_->get_FullScreen(&is_full_screen);
  return is_full_screen == VARIANT_TRUE;
}

HWND Browser::GetActiveDialogWindowHandle() {
  LOG(TRACE) << "Entering Browser::GetActiveDialogWindowHandle";

  HWND active_dialog_handle = NULL;

  HWND content_window_handle = this->GetContentWindowHandle();
  if (content_window_handle == NULL) {
    return active_dialog_handle;
  }

  DWORD process_id = 0;
  ::GetWindowThreadProcessId(content_window_handle, &process_id);
  if (process_id == 0) {
    return active_dialog_handle;
  }

  ProcessWindowInfo process_win_info;
  process_win_info.dwProcessId = process_id;
  process_win_info.hwndBrowser = NULL;
  ::EnumWindows(&BrowserFactory::FindDialogWindowForProcess,
                reinterpret_cast<LPARAM>(&process_win_info));
  if (process_win_info.hwndBrowser != NULL) {
    active_dialog_handle = process_win_info.hwndBrowser;
    this->CheckDialogType(active_dialog_handle);
  }

  return active_dialog_handle;
}

void Browser::CheckDialogType(HWND dialog_window_handle) {
  LOG(TRACE) << "Entering Browser::CheckDialogType";

  std::vector<char> window_class_name(34);
  if (GetClassNameA(dialog_window_handle, &window_class_name[0], 34)) {
    if (strcmp(HTML_DIALOG_WINDOW_CLASS,
        &window_class_name[0]) == 0) {
      HWND content_window_handle = this->FindContentWindowHandle(dialog_window_handle);
      ::PostMessage(this->executor_handle(),
                    WD_NEW_HTML_DIALOG,
                    NULL,
                    reinterpret_cast<LPARAM>(content_window_handle));
    }
  }
}

} // namespace webdriver
