// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome_frame/urlmon_moniker.h"

#include <exdisp.h>
#include <shlguid.h>

#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome_frame/bho.h"
#include "chrome_frame/bind_context_info.h"
#include "chrome_frame/chrome_active_document.h"
#include "chrome_frame/exception_barrier.h"
#include "chrome_frame/urlmon_bind_status_callback.h"
#include "chrome_frame/utils.h"
#include "chrome_frame/vtable_patch_manager.h"
#include "net/http/http_util.h"

static const int kMonikerBindToObject = 8;
static const int kMonikerBindToStorage = kMonikerBindToObject + 1;

base::LazyInstance<base::ThreadLocalPointer<NavigationManager> >
    NavigationManager::thread_singleton_ = LAZY_INSTANCE_INITIALIZER;

BEGIN_VTABLE_PATCHES(IMoniker)
  VTABLE_PATCH_ENTRY(kMonikerBindToObject, MonikerPatch::BindToObject)
  VTABLE_PATCH_ENTRY(kMonikerBindToStorage, MonikerPatch::BindToStorage)
END_VTABLE_PATCHES()

////////////////////////////

HRESULT NavigationManager::NavigateToCurrentUrlInCF(IBrowserService* browser) {
  DCHECK(browser);
  DVLOG(1) << __FUNCTION__ << " " << url();

  MarkBrowserOnThreadForCFNavigation(browser);

  HRESULT hr = S_OK;
  base::win::ScopedComPtr<IShellBrowser> shell_browser;
  base::win::ScopedComPtr<IBindCtx> bind_context;
  hr = ::CreateAsyncBindCtxEx(NULL, 0, NULL, NULL, bind_context.Receive(), 0);

  base::win::ScopedComPtr<IMoniker> moniker;
  DCHECK(bind_context);
  if (SUCCEEDED(hr) &&
      SUCCEEDED(hr = ::CreateURLMonikerEx(NULL, url_.c_str(), moniker.Receive(),
                                          URL_MK_UNIFORM))) {
    if (SUCCEEDED(hr)) {
      // If there's a referrer, preserve it.
      std::wstring headers;
      if (!referrer_.empty()) {
        headers = base::StringPrintf(L"Referer: %ls\r\n\r\n",
            ASCIIToWide(referrer_).c_str());
      }

      // Pass in URL fragments if applicable.
      std::wstring fragment;
      GURL parsed_moniker_url(url_);
      if (parsed_moniker_url.has_ref()) {
        fragment = UTF8ToWide(parsed_moniker_url.ref());
      }

      VARIANT flags = { VT_I4 };
      V_VT(&flags) = navNoHistory | navOpenInNewWindow;

      hr = NavigateBrowserToMoniker(browser, moniker, headers.c_str(),
          bind_context, fragment.c_str(), NULL, &flags);
      DVLOG(1) << base::StringPrintf("NavigateBrowserToMoniker: 0x%08X", hr);
    }
  }

  return hr;
}

bool NavigationManager::IsTopLevelUrl(const wchar_t* url) {
  return CompareUrlsWithoutFragment(url_.c_str(), url);
}

/////////////////////////////////////////

NavigationManager* NavigationManager::GetThreadInstance() {
  return thread_singleton_.Pointer()->Get();
}

void NavigationManager::RegisterThreadInstance() {
  DCHECK(GetThreadInstance() == NULL);
  thread_singleton_.Pointer()->Set(this);
}

void NavigationManager::UnregisterThreadInstance() {
  DCHECK(GetThreadInstance() == this);
  thread_singleton_.Pointer()->Set(NULL);
}

/////////////////////////////////////////

// static
bool MonikerPatch::Initialize() {
  if (IS_PATCHED(IMoniker)) {
    DLOG(WARNING) << __FUNCTION__ << " called more than once.";
    return true;
  }

  base::win::ScopedComPtr<IMoniker> moniker;
  HRESULT hr = ::CreateURLMoniker(NULL, L"http://localhost/",
                                  moniker.Receive());
  DCHECK(SUCCEEDED(hr));
  if (SUCCEEDED(hr)) {
    hr = vtable_patch::PatchInterfaceMethods(moniker, IMoniker_PatchInfo);
    DLOG_IF(ERROR, FAILED(hr)) << base::StringPrintf(
        "patch failed 0x%08X", hr);
  }

  return SUCCEEDED(hr);
}

// static
void MonikerPatch::Uninitialize() {
  vtable_patch::UnpatchInterfaceMethods(IMoniker_PatchInfo);
}

bool ShouldWrapCallback(IMoniker* moniker, REFIID iid, IBindCtx* bind_context) {
  CComHeapPtr<WCHAR> url;
  HRESULT hr = moniker->GetDisplayName(bind_context, NULL, &url);
  if (!url) {
    DVLOG(1) << __FUNCTION__
             << base::StringPrintf(" GetDisplayName failed. Error: 0x%x", hr);
    return false;
  }

  if (!IsEqualIID(IID_IStream, iid)) {
    DVLOG(1) << __FUNCTION__ << " Url: " << url
             << " Not wrapping: IID is not IStream.";
    return false;
  }

  base::win::ScopedComPtr<BindContextInfo> info;
  BindContextInfo::FromBindContext(bind_context, info.Receive());
  DCHECK(info);
  if (info && info->chrome_request()) {
    DVLOG(1) << __FUNCTION__ << " Url: " << url
             << " Not wrapping: request from chrome frame.";
    return false;
  }

  NavigationManager* mgr = NavigationManager::GetThreadInstance();
  if (!mgr) {
    DVLOG(1) << __FUNCTION__ << " Url: " << url
             << " No navigation manager to wrap";
    return false;
  }

  // Check whether request comes from MSHTML by checking for IInternetBindInfo.
  // We prefer to avoid wrapping if BindToStorage is called from AcroPDF.dll
  // (as a result of OnObjectAvailable)
  base::win::ScopedComPtr<IUnknown> bscb_holder;
  if (S_OK == bind_context->GetObjectParam(L"_BSCB_Holder_",
                                           bscb_holder.Receive())) {
    base::win::ScopedComPtr<IBindStatusCallback> bscb;
    if (S_OK != DoQueryService(IID_IBindStatusCallback, bscb_holder,
                               bscb.Receive()))
      return false;

    if (!bscb.get())
      return false;

    base::win::ScopedComPtr<IInternetBindInfo> bind_info;
    if (S_OK != bind_info.QueryFrom(bscb))
      return false;
  }

  // TODO(ananta)
  // Use the IsSubFrameRequest function to determine if a request is a top
  // level request. Something like this.
  // base::win::ScopedComPtr<IUnknown> bscb_holder;
  // bind_context->GetObjectParam(L"_BSCB_Holder_", bscb_holder.Receive());
  // if (bscb_holder) {
  //   base::win::ScopedComPtr<IHttpNegotiate> http_negotiate;
  //   http_negotiate.QueryFrom(bscb_holder);
  //   if (http_negotiate && !IsSubFrameRequest(http_negotiate))
  //     return true;
  //  }
  // There are some cases where the IsSubFrameRequest function can return
  // incorrect results.
  bool should_wrap = mgr->IsTopLevelUrl(url);
  if (!should_wrap) {
    DVLOG(1) << __FUNCTION__ << " Url: " << url
             << " Not wrapping: Not top level url.";
  }
  return should_wrap;
}

// static
HRESULT MonikerPatch::BindToObject(IMoniker_BindToObject_Fn original,
                                   IMoniker* me, IBindCtx* bind_ctx,
                                   IMoniker* to_left, REFIID iid, void** obj) {
  DVLOG(1) << __FUNCTION__;
  DCHECK(to_left == NULL);

  ExceptionBarrierReportOnlyModule barrier;

  HRESULT hr = S_OK;
  // Bind context is marked for switch when we sniff data in BSCBStorageBind
  // and determine that the renderer to be used is Chrome.
  base::win::ScopedComPtr<BindContextInfo> info;
  BindContextInfo::FromBindContext(bind_ctx, info.Receive());
  DCHECK(info);
  if (info) {
    if (info->is_switching()) {
      // We could implement the BindToObject ourselves here but instead we
      // simply register Chrome Frame ActiveDoc as a handler for 'text/html'
      // in this bind context.  This makes urlmon instantiate CF Active doc
      // instead of mshtml.
      const char* media_types[] = { "text/html" };
      CLSID classes[] = { CLSID_ChromeActiveDocument };
      hr = RegisterMediaTypeClass(bind_ctx, arraysize(media_types), media_types,
                                  classes, 0);
    } else {
      // In case the binding begins with BindToObject we do not need
      // to cache the data in the sniffing code.
      info->set_no_cache(true);
    }
  }

  hr = original(me, bind_ctx, to_left, iid, obj);
  return hr;
}

// static
HRESULT MonikerPatch::BindToStorage(IMoniker_BindToStorage_Fn original,
                                    IMoniker* me, IBindCtx* bind_ctx,
                                    IMoniker* to_left, REFIID iid, void** obj) {
  DCHECK(to_left == NULL);

  // Report a crash if the crash is in our own module.
  ExceptionBarrierReportOnlyModule barrier;

  HRESULT hr = S_OK;
  scoped_refptr<BSCBStorageBind> auto_release_callback;
  CComObject<BSCBStorageBind>* callback = NULL;
  if (ShouldWrapCallback(me, iid, bind_ctx)) {
    hr = CComObject<BSCBStorageBind>::CreateInstance(&callback);
    DCHECK(SUCCEEDED(hr));
    auto_release_callback = callback;
    DCHECK_EQ(callback->m_dwRef, 1);
    hr = callback->Initialize(me, bind_ctx);
    DCHECK(SUCCEEDED(hr));
  }

  hr = original(me, bind_ctx, to_left, iid, obj);

  // If the binding terminates before the data could be played back
  // now is the chance. Sometimes OnStopBinding happens after this returns
  // and then it's too late.
  if ((S_OK == hr) && callback)
    callback->MayPlayBack(BSCF_LASTDATANOTIFICATION);

  return hr;
}