// Copyright 2015 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/browser/ui/webui/print_preview/print_preview_distiller.h"

#include <stdint.h>
#include <string>
#include <utility>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/dom_distiller/tab_utils.h"
#include "chrome/browser/printing/print_preview_dialog_controller.h"
#include "chrome/browser/printing/print_preview_message_handler.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/web_contents_sizer.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/prerender_messages.h"
#include "components/dom_distiller/content/browser/distiller_javascript_utils.h"
#include "components/printing/common/print_messages.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/resource_request_details.h"
#include "content/public/browser/session_storage_namespace.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"

using content::OpenURLParams;
using content::RenderViewHost;
using content::SessionStorageNamespace;
using content::WebContents;

class PrintPreviewDistiller::WebContentsDelegateImpl
    : public content::WebContentsDelegate,
      public content::NotificationObserver,
      public content::WebContentsObserver {
 public:
  explicit WebContentsDelegateImpl(WebContents* web_contents,
                                   scoped_ptr<base::DictionaryValue> settings,
                                   const base::Closure on_failed_callback)
      : content::WebContentsObserver(web_contents),
        settings_(std::move(settings)),
        on_failed_callback_(on_failed_callback) {
    web_contents->SetDelegate(this);

    // Close ourselves when the application is shutting down.
    notification_registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING,
                                content::NotificationService::AllSources());

    // Register to inform new RenderViews that we're rendering.
    notification_registrar_.Add(
        this, content::NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED,
        content::Source<WebContents>(web_contents));
  }

  ~WebContentsDelegateImpl() override { web_contents()->SetDelegate(nullptr); }

  // content::WebContentsDelegate implementation.
  WebContents* OpenURLFromTab(WebContents* source,
                              const OpenURLParams& params) override {
    on_failed_callback_.Run();
    return nullptr;
  }

  void CloseContents(content::WebContents* contents) override {
    on_failed_callback_.Run();
  }

  void CanDownload(const GURL& url,
                   const std::string& request_method,
                   const base::Callback<void(bool)>& callback) override {
    on_failed_callback_.Run();
    // Cancel the download.
    callback.Run(false);
  }

  bool ShouldCreateWebContents(
      WebContents* web_contents,
      int32_t route_id,
      int32_t main_frame_route_id,
      int32_t main_frame_widget_route_id,
      WindowContainerType window_container_type,
      const std::string& frame_name,
      const GURL& target_url,
      const std::string& partition_id,
      SessionStorageNamespace* session_storage_namespace) override {
    // Since we don't want to permit child windows that would have a
    // window.opener property, terminate rendering.
    on_failed_callback_.Run();
    // Cancel the popup.
    return false;
  }

  bool OnGoToEntryOffset(int offset) override {
    // This isn't allowed because the history merge operation
    // does not work if there are renderer issued challenges.
    // TODO(cbentzel): Cancel in this case? May not need to do
    // since render-issued offset navigations are not guaranteed,
    // but indicates that the page cares about the history.
    return false;
  }

  bool ShouldSuppressDialogs(WebContents* source) override {
    // We still want to show the user the message when they navigate to this
    // page, so cancel this render.
    on_failed_callback_.Run();
    // Always suppress JavaScript messages if they're triggered by a page being
    // rendered.
    return true;
  }

  void RegisterProtocolHandler(WebContents* web_contents,
                               const std::string& protocol,
                               const GURL& url,
                               bool user_gesture) override {
    on_failed_callback_.Run();
  }

  void RenderFrameCreated(
      content::RenderFrameHost* render_frame_host) override {
    // When a new RenderFrame is created for a distilled rendering
    // WebContents, tell the new RenderFrame it's being used for
    // prerendering before any navigations occur.  Note that this is
    // always triggered before the first navigation, so there's no
    // need to send the message just after the WebContents is created.
    render_frame_host->Send(new PrerenderMsg_SetIsPrerendering(
        render_frame_host->GetRoutingID(), true));
  }

  void DidFinishLoad(content::RenderFrameHost* render_frame_host,
                     const GURL& validated_url) override {
    // Ask the page to trigger an anchor navigation once the distilled
    // contents are added to the page.
    dom_distiller::RunIsolatedJavaScript(
        web_contents()->GetMainFrame(),
        "navigate_on_initial_content_load = true;");
  }

  void DidNavigateMainFrame(
      const content::LoadCommittedDetails& details,
      const content::FrameNavigateParams& params) override {
    // The second content loads signals that the distilled contents have
    // been delivered to the page via inline JavaScript execution.
    if (web_contents()->GetController().GetEntryCount() > 1) {
      RenderViewHost* rvh = web_contents()->GetRenderViewHost();
      rvh->Send(new PrintMsg_InitiatePrintPreview(rvh->GetRoutingID(), false));
      rvh->Send(new PrintMsg_PrintPreview(rvh->GetRoutingID(), *settings_));
    }
  }

  void DidGetRedirectForResourceRequest(
      content::RenderFrameHost* render_frame_host,
      const content::ResourceRedirectDetails& details) override {
    if (details.resource_type != content::RESOURCE_TYPE_MAIN_FRAME)
      return;
    // Redirects are unsupported for distilled content renderers.
    on_failed_callback_.Run();
  }

  void RenderProcessGone(base::TerminationStatus status) override {
    on_failed_callback_.Run();
  }

  void Observe(int type,
               const content::NotificationSource& source,
               const content::NotificationDetails& details) override {
    switch (type) {
      // TODO(davidben): Try to remove this in favor of relying on
      // FINAL_STATUS_PROFILE_DESTROYED.
      case chrome::NOTIFICATION_APP_TERMINATING:
        on_failed_callback_.Run();
        return;

      case content::NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED: {
        if (web_contents()) {
          DCHECK_EQ(content::Source<WebContents>(source).ptr(), web_contents());

          // Make sure the size of the RenderViewHost has been passed to the new
          // RenderView.  Otherwise, the size may not be sent until the
          // RenderViewReady event makes it from the render process to the UI
          // thread of the browser process.  When the RenderView receives its
          // size, is also sets itself to be visible, which would then break the
          // visibility API.
          content::Details<RenderViewHost> new_render_view_host(details);
          new_render_view_host->GetWidget()->WasResized();
          web_contents()->WasHidden();
        }
        break;
      }

      default:
        NOTREACHED() << "Unexpected notification sent.";
        break;
    }
  }

 private:
  scoped_ptr<base::DictionaryValue> settings_;
  content::NotificationRegistrar notification_registrar_;

  // The callback called when the preview failed.
  base::Closure on_failed_callback_;
};

const base::Feature PrintPreviewDistiller::kFeature = {
    "PrintPreviewDistiller", base::FEATURE_ENABLED_BY_DEFAULT,
};

bool PrintPreviewDistiller::IsEnabled() {
  return base::FeatureList::IsEnabled(kFeature);
}

PrintPreviewDistiller::PrintPreviewDistiller(
    WebContents* source_web_contents,
    const base::Closure on_failed_callback,
    scoped_ptr<base::DictionaryValue> settings) {
  content::SessionStorageNamespace* session_storage_namespace =
      source_web_contents->GetController().GetDefaultSessionStorageNamespace();
  CreateDestinationWebContents(session_storage_namespace, source_web_contents,
                               std::move(settings), on_failed_callback);

  DCHECK(web_contents_);
  ::DistillAndView(source_web_contents, web_contents_.get());
}

void PrintPreviewDistiller::CreateDestinationWebContents(
    SessionStorageNamespace* session_storage_namespace,
    WebContents* source_web_contents,
    scoped_ptr<base::DictionaryValue> settings,
    const base::Closure on_failed_callback) {
  DCHECK(!web_contents_);

  web_contents_.reset(
      CreateWebContents(session_storage_namespace, source_web_contents));

  printing::PrintPreviewMessageHandler::CreateForWebContents(
      web_contents_.get());

  web_contents_delegate_.reset(new WebContentsDelegateImpl(
      web_contents_.get(), std::move(settings), on_failed_callback));

  // Set the size of the distilled WebContents.
  ResizeWebContents(web_contents_.get(), gfx::Size(1, 1));

  printing::PrintPreviewDialogController* dialog_controller =
      printing::PrintPreviewDialogController::GetInstance();
  if (!dialog_controller)
    return;

  dialog_controller->AddProxyDialogForWebContents(web_contents_.get(),
                                                  source_web_contents);
}

PrintPreviewDistiller::~PrintPreviewDistiller() {
  DCHECK(web_contents_);

  printing::PrintPreviewDialogController* dialog_controller =
      printing::PrintPreviewDialogController::GetInstance();
  if (!dialog_controller)
    return;

  dialog_controller->RemoveProxyDialogForWebContents(web_contents_.get());
}

WebContents* PrintPreviewDistiller::CreateWebContents(
    SessionStorageNamespace* session_storage_namespace,
    WebContents* source_web_contents) {
  // TODO(ajwong): Remove the temporary map once prerendering is aware of
  // multiple session storage namespaces per tab.
  content::SessionStorageNamespaceMap session_storage_namespace_map;
  Profile* profile =
      Profile::FromBrowserContext(source_web_contents->GetBrowserContext());
  session_storage_namespace_map[std::string()] = session_storage_namespace;
  return WebContents::CreateWithSessionStorage(
      WebContents::CreateParams(profile), session_storage_namespace_map);
}