// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/icon_transcoder/svg_icon_transcoder.h"

#include "base/base64.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "base/strings/string_view_util.h"
#include "base/task/thread_pool.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/web_contents.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"

namespace apps {

namespace {

constexpr char kSvgDataUrlPrefix[] = "data:image/svg+xml;base64,";

std::string ReadSvgOnFileThread(base::FilePath svg_path) {
  std::string svg_data;
  if (base::PathExists(svg_path)) {
    base::ReadFileToString(svg_path, &svg_data);
    LOG_IF(ERROR, svg_data.empty()) << "Empty svg data at path " << svg_path;
  }
  return svg_data;
}

void SaveIconOnFileThread(const base::FilePath& icon_path,
                          const std::string& content) {
  DCHECK(!content.empty());

  base::File::Error file_error;
  if (!base::CreateDirectoryAndGetError(icon_path.DirName(), &file_error)) {
    LOG(ERROR) << "Failed to create dir " << icon_path.DirName()
               << " with error " << file_error;
    return;
  }

  if (!base::WriteFile(icon_path, content)) {
    LOG(ERROR) << "Failed to write icon file: " << icon_path;
    if (!base::DeleteFile(icon_path)) {
      LOG(ERROR) << "Couldn't delete broken icon file" << icon_path;
    }
  }
}

}  // namespace

SvgIconTranscoder::SvgIconTranscoder(content::BrowserContext* context)
    : browser_context_(context) {}

SvgIconTranscoder::~SvgIconTranscoder() {
  RemoveObserver();
}

// Reads the svg data at svg_path and invokes the string Transcode method.
// |callback| is invoked with and empty string on failure. Blocking call.
void SvgIconTranscoder::Transcode(const base::FilePath&& svg_path,
                                  const base::FilePath&& png_path,
                                  gfx::Size preferred_size,
                                  IconContentCallback callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&ReadSvgOnFileThread, std::move(svg_path)),
      base::BindOnce(
          [](base::WeakPtr<SvgIconTranscoder> weak_this,
             const base::FilePath&& png_path, gfx::Size preferred_size,
             IconContentCallback callback, std::string svg_data) {
            if (weak_this && !svg_data.empty()) {
              weak_this->Transcode(std::move(svg_data), std::move(png_path),
                                   preferred_size, std::move(callback));
              return;
            }

            std::move(callback).Run(std::string());
          },
          GetWeakPtr(), std::move(png_path), preferred_size,
          std::move(callback)));
}

// Validates and trims the svg_data before base64 encoding and dispatching to
// |web_contents_| in a data: URI.  |callback| is invoked with and empty
// string on failure. Blocking call.
void SvgIconTranscoder::Transcode(const std::string& svg_data,
                                  const base::FilePath&& png_path,
                                  gfx::Size preferred_size,
                                  IconContentCallback callback) {
  if (!PrepareWebContents()) {
    LOG(ERROR) << "Can't transcode svg. WebContents not ready.";
    std::move(callback).Run(std::string());
    return;
  }

  auto pos = svg_data.find("<svg");
  if (pos == std::string::npos) {
    LOG(ERROR) << "Invalid data. Couldn't find <svg.";
    std::move(callback).Run(std::string());
    return;
  }
  // Form a data: uri from the svg_data starting at the <svg. Excess ASCII
  // whitespace is also removed.
  std::string base64_svg = base::Base64Encode(
      base::CollapseWhitespaceASCII(svg_data.substr(pos), false));

  GURL data_url(kSvgDataUrlPrefix + base64_svg);

  web_contents_->DownloadImage(
      data_url, /*is_favicon=*/false, preferred_size, /*max_bitmap_size=*/0,
      /*bypass_cache=*/true,
      base::BindOnce(&SvgIconTranscoder::OnDownloadImage, GetWeakPtr(),
                     std::move(png_path), std::move(callback)));
}

void SvgIconTranscoder::MaybeCreateWebContents() {
  if (!web_contents_) {
    auto params = content::WebContents::CreateParams(browser_context_);
    params.initially_hidden = true;
    params.desired_renderer_state =
        content::WebContents::CreateParams::kInitializeAndWarmupRendererProcess;
    web_contents_ = content::WebContents::Create(params);
    // When we observe RenderProcessExited, we will need to recreate.
    web_contents_->GetPrimaryMainFrame()->GetProcess()->AddObserver(this);
  }
}

bool SvgIconTranscoder::PrepareWebContents() {
  if (!web_contents_ready_) {
    // Old web_contents_ may have been destroyed.
    MaybeCreateWebContents();
    if (web_contents_->GetPrimaryMainFrame()->IsRenderFrameLive()) {
      web_contents_ready_ = true;
    }
    VLOG(1) << "web_contents "
            << (web_contents_ready_ ? "ready " : "not ready");
  }
  return web_contents_ready_;
}

void SvgIconTranscoder::RenderProcessReady(content::RenderProcessHost* host) {
  web_contents_ready_ = true;
}

void SvgIconTranscoder::RenderProcessExited(
    content::RenderProcessHost* host,
    const content::ChildProcessTerminationInfo& info) {
  web_contents_ready_ = false;
  RemoveObserver();
  web_contents_.reset();
}

void SvgIconTranscoder::RemoveObserver() {
  if (web_contents_ && web_contents_->GetPrimaryMainFrame()) {
    web_contents_->GetPrimaryMainFrame()->GetProcess()->RemoveObserver(this);
  }
}

// Compresses the first received bitmap and  saves compressed data to
// |png_path| if non-empty. If the file can't be saved, that's not considered
// and error. Next time lucky.
void SvgIconTranscoder::OnDownloadImage(base::FilePath png_path,
                                        IconContentCallback callback,
                                        int id,
                                        int http_status_code,
                                        const GURL& image_url,
                                        const std::vector<SkBitmap>& bitmaps,
                                        const std::vector<gfx::Size>& sizes) {
  if (bitmaps.empty()) {
    VLOG(1) << "status " << http_status_code << " for download id " << id;
    VLOG(1) << "Failed to download image from " << image_url;
    std::move(callback).Run(std::string());
    return;
  }

  const SkBitmap& bitmap = bitmaps[0];

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(
          [](const SkBitmap& bitmap) {
            if (bitmap.empty()) {
              return std::string();
            }
            std::optional<std::vector<uint8_t>> compressed =
                gfx::PNGCodec::EncodeBGRASkBitmap(
                    bitmap, /*discard_transparency=*/false);
            return std::string(base::as_string_view(
                compressed.value_or(std::vector<uint8_t>())));
          },
          bitmap),
      base::BindOnce(
          [](base::FilePath png_path, IconContentCallback callback,
             std::string compressed) {
            if (!compressed.empty() && !png_path.empty()) {
              base::ThreadPool::PostTaskAndReply(
                  FROM_HERE,
                  {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
                  base::BindOnce(&SaveIconOnFileThread, std::move(png_path),
                                 compressed),
                  base::BindOnce(std::move(callback), compressed));
            } else {
              std::move(callback).Run(std::move(compressed));
            }
          },
          std::move(png_path), std::move(callback)));
}

}  // namespace apps
