/**
 * Event: OnAttachForgeExtensions
 */
void __stdcall CBrowserHelperObject::OnAttachForgeExtensions(IDispatch *dispatch, const wstring& location, const wstring& eventsource)
{
	m_isAttached = false;
  auto manifest = _AtlModule.moduleManifest;
  CComPtr<IDispatch> disp = nullptr;
  CComQIPtr<IHTMLWindow2, &IID_IHTMLWindow2> htmlWindow2 = nullptr;
  HRESULT hr = S_OK;

  for (;;) {
    CComQIPtr<IWebBrowser2, &IID_IWebBrowser2> webBrowser2(dispatch);
    BreakOnNullWithErrorLog(webBrowser2, L"BrowserHelperObject::OnAttachForgeExtensions failed to obtain IWebBrowser2");

    logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions -> " + boost::lexical_cast<wstring>(dispatch) +  L" -> " + location + L" -> " + eventsource);
    // get interfaces
    hr = webBrowser2->get_Document(&disp);
    BreakOnNullWithErrorLog(disp, L"BrowserHelperObject::OnAttachForgeExtensions get_Document failed");
    BreakOnFailed(hr);
    
    logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions IDispatch -> " + boost::lexical_cast<wstring>(disp));

    CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> htmlDocument2(disp);
    BreakOnNullWithErrorLog(htmlDocument2, L"BrowserHelperObject::OnAttachForgeExtensions IHTMLDocument2 failed");

    logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions IHTMLDocument2 -> " + boost::lexical_cast<wstring>(htmlDocument2));

    hr = htmlDocument2->get_parentWindow(&htmlWindow2);
    BreakOnNullWithErrorLog(htmlWindow2, L"BrowserHelperObject::OnAttachForgeExtensions IHTMLWindow2 failed");
    BreakOnFailed(hr);
    
    logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions IHTMLWindow2 -> " + boost::lexical_cast<wstring>(htmlWindow2));

    CComQIPtr<IDispatchEx> htmlWindow2Ex(htmlWindow2);
    BreakOnNullWithErrorLog(htmlWindow2Ex, L"BrowserHelperObject::OnAttachForgeExtensions IHTMLWindow2Ex failed");

    // Attach NativeAccessible (forge.tabs.*)
    if (m_nativeAccessible) {
      logger->error(L"BrowserHelperObject::OnAttachForgeExtensions resetting nativeAccessible");
      m_nativeAccessible.reset();
    }

    m_nativeAccessible = NativeAccessible::pointer(new NativeAccessible(webBrowser2));
    hr = Attach::NativeTabs(htmlWindow2Ex, L"accessible", m_nativeAccessible.get());
    BreakOnFailedWithErrorLog(hr, L"BrowserHelperObject::OnAttachForgeExtensions failed to attach NativeExtensions -> " + logger->parse(hr));

    // Attach NativeExtensions
    hr = Attach::NativeExtensions(manifest->uuid, htmlWindow2Ex, L"extensions", m_instanceId, location, &m_nativeExtensions.p);
    BreakOnFailedWithErrorLog(hr, L"BrowserHelperObject::OnAttachForgeExtensions failed to attach NativeExtensions -> " + logger->parse(hr));
    
    logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions attached NativeExtensions");

    // Attach NativeMessaging
    hr = Attach::NativeMessaging(manifest->uuid, htmlWindow2Ex, L"messaging", m_instanceId, &m_nativeMessaging.p);
    BreakOnFailedWithErrorLog(hr, L"BrowserHelperObject::OnAttachForgeExtensions failed to attach NativeMessaging -> " + logger->parse(hr));

    /// finally
    logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions attached NativeMessaging");
    m_isAttached = true;
    break;
  }
}
/**
 * Event: OnAttachForgeExtensions
 */
void __stdcall CBrowserHelperObject::OnAttachForgeExtensions(IDispatch *dispatch, 
															 const wstring& location,
															 const wstring& eventsource)
{
	m_isAttached = false;

	CComQIPtr<IWebBrowser2, &IID_IWebBrowser2> webBrowser2(dispatch);
    if (!webBrowser2) {
        logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions "
                      L"failed to obtain IWebBrowser2");
        return;
    } 

	logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions"
				  L" -> " + boost::lexical_cast<wstring>(dispatch) +
				  L" -> " + location +
				  L" -> " + eventsource);

    Manifest::pointer manifest = _AtlModule.moduleManifest;

    // get interfaces
    CComPtr<IDispatch> disp;
    webBrowser2->get_Document(&disp);
    if (!disp) {
        logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions get_Document failed");
        return;
    }
	logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions "
				  L" IDispatch -> " + boost::lexical_cast<wstring>(disp)); 
    CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> htmlDocument2(disp);
    if (!htmlDocument2) {
        logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions IHTMLDocument2 failed");
        return;
    }
	logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions "
				  L" IHTMLDocument2 -> " + boost::lexical_cast<wstring>(htmlDocument2)); 
    CComQIPtr<IHTMLWindow2, &IID_IHTMLWindow2> htmlWindow2;
    htmlDocument2->get_parentWindow(&htmlWindow2);    
    if (!htmlWindow2) {
        logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions IHTMLWindow2 failed");
        return;
    }
	logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions "
				  L" IHTMLWindow2 -> " + boost::lexical_cast<wstring>(htmlWindow2)); 
    CComQIPtr<IDispatchEx> htmlWindow2Ex(htmlWindow2);
    if (!htmlWindow2Ex) {
        logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions IHTMLWindow2Ex failed");
        return;
    }

    HRESULT hr;

    // Attach NativeAccessible (forge.tabs.*)
    if (m_nativeAccessible) {
        logger->error(L"BrowserHelperObject::OnAttachForgeExtensions resetting nativeAccessible");
        m_nativeAccessible.reset();
    }
    m_nativeAccessible = NativeAccessible::pointer(new NativeAccessible(webBrowser2));
    hr = Attach::NativeTabs(htmlWindow2Ex, 
                            L"accessible",
                            m_nativeAccessible.get());
    if (FAILED(hr)) {
        logger->error(L"BrowserHelperObject::OnAttachForgeExtensions "
                      L"failed to attach NativeExtensions"
                      L" -> " + logger->parse(hr));
        return;
    }    

    // Attach NativeExtensions
    hr = Attach::NativeExtensions(manifest->uuid,
                                  htmlWindow2Ex, 
                                  L"extensions", 
                                  m_instanceId,
                                  location,
                                  &m_nativeExtensions.p); // "T** operator&() throw()" asserts on p==NULL
    if (FAILED(hr)) {
        logger->error(L"BrowserHelperObject::OnAttachForgeExtensions "
                      L"failed to attach NativeExtensions"
                      L" -> " + logger->parse(hr));
        return;
    }
	logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions "
				  L"attached NativeExtensions");

    // Attach NativeMessaging
    hr = Attach::NativeMessaging(manifest->uuid,
                                 htmlWindow2Ex, 
                                 L"messaging", 
                                 m_instanceId,
                                 &m_nativeMessaging.p); // "T** operator&() throw()" asserts on p==NULL
    if (FAILED(hr)) {
        logger->error(L"BrowserHelperObject::OnAttachForgeExtensions "
                      L"failed to attach NativeMessaging"
                      L" -> " + logger->parse(hr));
        return;
    }

	logger->debug(L"BrowserHelperObject::OnAttachForgeExtensions "
				  L"attached NativeMessaging");

	m_isAttached = true;
}
/**
 * Event: OnDocumentComplete
 */
void __stdcall CBrowserHelperObject::OnDocumentComplete(IDispatch *dispatch,
                                                        VARIANT   *url)
{
    Manifest::pointer manifest = _AtlModule.moduleManifest;

    CComQIPtr<IWebBrowser2, &IID_IWebBrowser2> webBrowser2(dispatch);
    if (!webBrowser2) {
        logger->debug(L"BrowserHelperObject::OnDocumentComplete "
                      L"failed to obtain IWebBrowser2");
        return;
    } 

    // VARIANT *url chops off file:\\\ for local filesystem
    CComBSTR bstr;
    webBrowser2->get_LocationURL(&bstr); 
    wstring location(bstr);
	if (location == L"" && url->bstrVal) { // get_LocationURL fails in the most egregious of ways
		location = url->bstrVal;
	}
	if (location == L"" && url->bstrVal == NULL) {
        logger->debug(L"BrowserHelperObject::OnDocumentComplete "
					  L"blank location, not interested"
                      L" -> " + manifest->uuid +
					  L" -> " + location);
		return;
	}

    // match location against manifest 
    std::pair<wstringvector, wstringvector> match = this->MatchManifest(webBrowser2, manifest, location);
    if (match.first.size() == 0 && match.second.size() == 0) {
        logger->debug(L"BrowserHelperObject::OnDocumentComplete not interested"
                      L" -> " + manifest->uuid +
					  L" -> " + wstring(url->bstrVal) +
                      L" -> " + location);
        return;
    }

    logger->debug(L"BrowserHelperObject::OnDocumentComplete"
                  L" -> " + manifest->uuid +
				  L" -> " + wstring(url->bstrVal) +
                  L" -> " + location);

    // get IHTMLWindow2
    CComPtr<IDispatch> disp;
    webBrowser2->get_Document(&disp);
    if (!disp) {
        logger->debug(L"BrowserHelperObject::OnDocumentComplete get_Document failed");
        return;
    }
    CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> htmlDocument2(disp);
    if (!htmlDocument2) {
        logger->debug(L"BrowserHelperObject::OnDocumentComplete IHTMLDocument2 failed");
        return;
    }
    CComQIPtr<IHTMLWindow2, &IID_IHTMLWindow2> htmlWindow2;
    htmlDocument2->get_parentWindow(&htmlWindow2);    
    if (!htmlWindow2) {
        logger->debug(L"BrowserHelperObject::OnDocumentComplete IHTMLWindow2 failed");
        return;
    }

	// attach forge extensions when pages like target=_blank didn't trigger navComplete event
	if (!m_isAttached) {
		this->OnAttachForgeExtensions(dispatch, location, L"OnDocumentComplete");
	}

    // Inject styles
    wstringset dupes;
    HTMLDocument document(webBrowser2); 
    ScriptExtensions::scriptvector matches = m_scriptExtensions->read_styles(match.first);
    ScriptExtensions::scriptvector::const_iterator i = matches.begin();
    for (; i != matches.end(); i++) {
        if (dupes.find(i->first) != dupes.end()) {
            logger->debug(L"BrowserHelperObject::OnDocumentComplete already injected -> " + i->first);
            continue;
        }
        wstringpointer style = i->second;
        if (!style) {
            logger->debug(L"BrowserHelperObject::OnDocumentComplete invalid stylesheet -> " + i->first);
            continue;
        }
        HRESULT hr;
        hr = document.InjectStyle(style);
        if (FAILED(hr)) {
            logger->error(L"BrowserHelperObject::OnDocumentComplete failed to inject style"
                          L" -> " + i->first +
                          L" -> " + logger->parse(hr));
            continue;
        }
        dupes.insert(i->first);
        logger->debug(L"BrowserHelperObject::OnDocumentComplete injected: " + i->first);
    }    

    // Inject scripts
    dupes.clear();
    matches = m_scriptExtensions->read_scripts(match.second);
    i = matches.begin();
    for (; i != matches.end(); i++) {
        if (dupes.find(i->first) != dupes.end()) {
            logger->debug(L"BrowserHelperObject::OnDocumentComplete already injected -> " + i->first);
            continue;
        }
        wstringpointer script = i->second;
        if (!script) {
            logger->debug(L"BrowserHelperObject::OnDocumentComplete invalid script -> " + i->first);
            continue;
        }
        HRESULT hr;
        //hr = document.InjectScript(script);
        //hr = document.InjectScriptTag(HTMLDocument::attrScriptType, i->first);
        CComVariant ret;
        hr = htmlWindow2->execScript(CComBSTR(script->c_str()), L"javascript", &ret);
        if (FAILED(hr)) {
            logger->error(L"BrowserHelperObject::OnDocumentComplete failed to inject script"
                          L" -> " + i->first +
                          L" -> " + logger->parse(hr));
            continue;
        }
        dupes.insert(i->first);
        logger->debug(L"BrowserHelperObject::OnDocumentComplete injected"
                      L" -> " + location +
                      L" -> " + i->first);
    }

    /*// Test in-process IAccessible access
    SHANDLE_PTR phwnd;
    hr = webBrowser2->get_HWND(&phwnd);
    HWND hwnd = reinterpret_cast<HWND>(phwnd);
    AccessibleBrowser ab(hwnd);
    wstringvector tabs = ab.tabs();
    for (wstringvector::const_iterator tab = tabs.begin(); tab != tabs.end(); tab++) {
        logger->debug(L"BrowserHelperObject::OnDocumentComplete -> " + *tab);
        }*/
}
/**
 * Helper: MatchManifest -> IWebBrowser2 -> Manifest -> location -> (styles . scripts)
 */
std::pair<wstringvector, wstringvector> 
CBrowserHelperObject::MatchManifest(IWebBrowser2 *webBrowser2,
                                    const Manifest::pointer& manifest, 
                                    const wstring& location)
{
    // Do nothing for special urls
    if (!(location.find(L"http:")  == 0 ||
          location.find(L"https:") == 0 ||
          location.find(L"file:")  == 0 /*||
		  location.find(L"about:") == 0*/)) {
        logger->debug(L"BrowserHelperObject::MatchManifest boring url"
                      L" -> " + location);        
        return std::pair<wstringvector, wstringvector>();
    }

    // is this a html document?
    CComPtr<IDispatch> disp;
    webBrowser2->get_Document(&disp);
    if (!disp) {
        logger->debug(L"BrowserHelperObject::MatchManifest "
                      L"no document");
        return std::pair<wstringvector, wstringvector>();
    }
    CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> htmlDocument2(disp);
    if (!htmlDocument2) {
        logger->debug(L"BrowserHelperObject::MatchManifest "
                      L"not an IHTMLDocument2");
        return std::pair<wstringvector, wstringvector>();
    }    

    // is this an iframe ?
    wstring locationtop = location;
    bool is_not_iframe = m_webBrowser2.IsEqualObject(webBrowser2);
    if (is_not_iframe) {
    } else {
        CComPtr<IDispatch> disp_parent;
        HRESULT hr = webBrowser2->get_Parent(&disp_parent);
        if (FAILED(hr) || !disp_parent) {
            logger->debug(L"BrowserHelperObject::MatchManifest not an iframe 1"
                          L" -> " + location);
            return std::pair<wstringvector, wstringvector>();
        }
        CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> htmlDocument2_parent(disp_parent);
        if (!htmlDocument2_parent) {
            logger->debug(L"BrowserHelperObject::MatchManifest not an iframe 2"
                          L" -> " + location);
            return std::pair<wstringvector, wstringvector>();
        }
        CComBSTR bstr;
        htmlDocument2_parent->get_URL(&bstr);
        locationtop = bstr;
    }

    // check content_scripts.matches, content_scripts.all_frames
    std::pair<wstringvector, wstringvector> ret;
    Manifest::ContentScripts::const_iterator script = manifest->content_scripts.begin();
    for (; script != manifest->content_scripts.end(); script++) {

		// check for exclude_matches
		bool exclude = false;
		wstringvector::const_iterator exclude_match = script->exclude_matches.begin();
		for (; exclude_match != script->exclude_matches.end(); exclude_match++) {
			if (wstring_match_wild(*exclude_match, location)) {
				logger->debug(L"BrowserHelperObject::MatchManifest exclude "
							  L" -> " + *exclude_match + L" -> " + location);
				exclude = true;
			}
		}

		// check for matches and add if, and only if there are no excludes for this activation
        wstringvector::const_iterator match = script->matches.begin();
        for (; match != script->matches.end(); match++) {
            bool matches = wstring_match_wild(*match, location);
            if ((matches && is_not_iframe && !exclude) ||        // matches top-level ||
                (matches && script->all_frames && !exclude)) {   // matches iframe  
                ret.first.insert(ret.first.end(), script->css.begin(), script->css.end());
                ret.second.insert(ret.second.end(), script->js.begin(), script->js.end());
            }
        }
    }

    return ret;
}
// Implement refresh(F5 click, etc) based on
// http://www.codeproject.com/Articles/3632/Detecting-the-IE-Refresh-button-using-IWebBrowser2
void CBrowserHelperObject::OnRefresh()
{
   auto manifest = _AtlModule.moduleManifest;
   wstring location;
   wstringset dupes;

   CComBSTR bstr = nullptr;
   CComPtr<IDispatch> disp = nullptr;
   CComQIPtr<IHTMLWindow2, &IID_IHTMLWindow2> htmlWindow2 = nullptr;
   
   HRESULT hr = S_OK;
   
   for (;;) {
     // VARIANT *url chops off file:\\\ for local filesystem    
     CComQIPtr<IWebBrowser2, &IID_IWebBrowser2> webBrowser2(m_webBrowser2);
     BreakOnNull(webBrowser2, hr);
     hr = webBrowser2->get_LocationURL(&bstr);
     BreakOnFailed(hr);
     location = wstring(bstr); // was m_strUrl
     if (location.empty()) {
       logger->debug(L"BrowserHelperObject::OnRefresh blank location, not interested  -> " + manifest->uuid + L" -> " + location);
       break;
     }

     // match location against manifest 
     auto& match = MatchManifest(webBrowser2, manifest, location);
     if (match.first.empty() && match.second.empty()) {
       logger->debug(L"BrowserHelperObject::OnRefresh not interested -> " + manifest->uuid + L" -> " + location);
       break;
     }
     logger->debug(L"BrowserHelperObject::OnRefresh -> " + manifest->uuid + L" -> " + location);

     // get IHTMLWindow2
     hr = webBrowser2->get_Document(&disp);
     BreakOnNullWithErrorLog(disp, L"BrowserHelperObject::OnRefresh get_Document failed");
     BreakOnFailed(hr);

     CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> htmlDocument2(disp);
     BreakOnNullWithErrorLog(htmlDocument2, L"BrowserHelperObject::OnRefresh IHTMLDocument2 failed");

     hr = htmlDocument2->get_parentWindow(&htmlWindow2);
     BreakOnNullWithErrorLog(htmlWindow2, L"BrowserHelperObject::OnRefresh IHTMLWindow2 failed");
     BreakOnFailed(hr);

     // attach forge extensions when pages like target=_blank didn't trigger navComplete event
     if (!m_isAttached)
       OnAttachForgeExtensions(m_webBrowser2, location, L"OnRefresh"); // was L"OnDocumentComplete"

     // Inject styles
     HTMLDocument document(webBrowser2);
     auto& matches = m_scriptExtensions->read_styles(match.first);
     for (auto& i : matches) {
       if (dupes.find(i.first) != dupes.end()) {
         logger->debug(L"BrowserHelperObject::OnRefresh already injected -> " + i.first);
         continue;
       }
       auto style = i.second;
       if (!style) {
         logger->debug(L"BrowserHelperObject::OnRefresh invalid stylesheet -> " + i.first);
         continue;
       }
       hr = document.InjectStyle(style);
       if (FAILED(hr)) {
         logger->error(L"BrowserHelperObject::OnRefresh failed to inject style -> " + i.first + L" -> " + logger->parse(hr));
         continue;
       }
       dupes.insert(i.first);
       logger->debug(L"BrowserHelperObject::OnRefresh injected: " + i.first);
     }

     // Inject scripts
     dupes.clear();
     matches = m_scriptExtensions->read_scripts(match.second);
     for (auto& i : matches) {
       if (dupes.find(i.first) != dupes.end()) {
         logger->debug(L"BrowserHelperObject::OnRefresh already injected -> " + i.first);
         continue;
       }
       auto script = i.second;
       if (!script) {
         logger->debug(L"BrowserHelperObject::OnRefresh invalid script -> " + i.first);
         continue;
       }
       CComVariant ret;
       hr = htmlWindow2->execScript(CComBSTR(script->c_str()), L"javascript", &ret);
       if (FAILED(hr)) {
         logger->error(L"BrowserHelperObject::OnRefresh failed to inject script -> " + i.first + L" -> " + logger->parse(hr));
         continue;
       }
       dupes.insert(i.first);
       logger->debug(L"BrowserHelperObject::OnRefresh injected -> " + location + L" -> " + i.first);
     }

     break;
   }   
}
/**
 * Helper: MatchManifest -> IWebBrowser2 -> Manifest -> location -> (styles . scripts)
 */
std::pair<wstringvector, wstringvector> 
CBrowserHelperObject::MatchManifest(IWebBrowser2 *webBrowser2, const Manifest::pointer& manifest,  const wstring& location)
{
  CComPtr<IDispatch> disp = nullptr;
  CComPtr<IDispatch> disp_parent = nullptr;
  CComBSTR bstr = nullptr;
  std::pair<wstringvector, wstringvector> ret;
  HRESULT hr = S_OK;

  for (;;) {
    // Do nothing for special urls
    if (!(location.find(L"http:") == 0 ||
      location.find(L"https:") == 0 ||
      location.find(L"file:") == 0)) {
      logger->debug(L"BrowserHelperObject::MatchManifest boring url -> " + location);
      break;
    }

    // is this a html document?    
    hr = webBrowser2->get_Document(&disp);
    BreakOnNullWithErrorLog(disp, L"BrowserHelperObject::MatchManifest no document");
    BreakOnFailed(hr);
    
    CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> htmlDocument2(disp);
    BreakOnNullWithErrorLog(htmlDocument2, L"BrowserHelperObject::MatchManifest not an IHTMLDocument2");

    // is this an iframe ?
    wstring locationtop = location;
    bool const is_not_iframe = m_webBrowser2.IsEqualObject(webBrowser2);
    if (!is_not_iframe) {
      hr = webBrowser2->get_Parent(&disp_parent);
      BreakOnNullWithErrorLog(disp_parent, L"BrowserHelperObject::MatchManifest not an iframe 1 -> " + location);
      BreakOnFailed(hr);

      CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> htmlDocument2_parent(disp_parent);
      BreakOnNullWithErrorLog(htmlDocument2_parent, L"BrowserHelperObject::MatchManifest not an iframe 2 -> " + location);
      
      hr = htmlDocument2_parent->get_URL(&bstr);
      BreakOnFailed(hr);
      locationtop = bstr;
    }

    // check content_scripts.matches, content_scripts.all_frames
    for (auto& script : manifest->content_scripts) {
      // check for exclude_matches
      bool exclude = false;
      for (auto& exclude_match : script.exclude_matches) 
        if (wstring_match_wild(exclude_match, location)) {
          logger->debug(L"BrowserHelperObject::MatchManifest exclude -> " + exclude_match + L" -> " + location);
          exclude = true;
        }

      // check for matches and add if, and only if there are no excludes for this activation
      for (auto& match : script.matches) {
        bool matches = wstring_match_wild(match, location);
        if ( (is_not_iframe || script.all_frames) && matches && !exclude) {
          ret.first.insert(ret.first.end(), script.css.begin(), script.css.end());
          ret.second.insert(ret.second.end(), script.js.begin(), script.js.end());
        }
      }
    }
    
    break;
  }

  return ret;
}